├── .gitignore ├── LICENSE ├── README.md ├── VideoMeme.xcodeproj └── project.pbxproj └── VideoMeme ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── ContentView.swift ├── Info.plist ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── Rendering ├── VideoComposition.swift └── VideoRenderer.swift ├── VideoMeme.entitlements ├── VideoMemeApp.swift ├── VideoMemeDocument.swift └── VideoViewModel.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __MACOSX 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | DerivedData 17 | .idea/ 18 | generatechangelog.sh 19 | Pods/ 20 | Carthage 21 | Provisioning -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Guilherme Rambo 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoMeme 2 | 3 | A sample project showing how a document-based app can be created with SwiftUI. 4 | 5 | [See it in action](https://youtu.be/eGT6Aj0kVgA). 6 | 7 | [Learn more](https://wwdcbysundell.com/2020/creating-document-based-apps-in-swiftui). 8 | 9 | **Requires macOS Big Sur and Xcode 12.** 10 | 11 | _NOTE: The code in this app is sample® code®, so it shouldn't be used for any purposes other than learning._ -------------------------------------------------------------------------------- /VideoMeme.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DD2091D824A645F200309C06 /* VideoMemeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2091D724A645F200309C06 /* VideoMemeApp.swift */; }; 11 | DD2091DA24A645F200309C06 /* VideoMemeDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2091D924A645F200309C06 /* VideoMemeDocument.swift */; }; 12 | DD2091DC24A645F200309C06 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2091DB24A645F200309C06 /* ContentView.swift */; }; 13 | DD2091DE24A645F800309C06 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD2091DD24A645F800309C06 /* Assets.xcassets */; }; 14 | DD2091E124A645F800309C06 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD2091E024A645F800309C06 /* Preview Assets.xcassets */; }; 15 | DD2091EC24A6487500309C06 /* VideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2091EA24A6487500309C06 /* VideoRenderer.swift */; }; 16 | DD2091ED24A6487500309C06 /* VideoComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2091EB24A6487500309C06 /* VideoComposition.swift */; }; 17 | DD2091EF24A648D000309C06 /* VideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2091EE24A648D000309C06 /* VideoViewModel.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | DD2091D424A645F200309C06 /* VideoMeme.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VideoMeme.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | DD2091D724A645F200309C06 /* VideoMemeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoMemeApp.swift; sourceTree = ""; }; 23 | DD2091D924A645F200309C06 /* VideoMemeDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoMemeDocument.swift; sourceTree = ""; }; 24 | DD2091DB24A645F200309C06 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 25 | DD2091DD24A645F800309C06 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | DD2091E024A645F800309C06 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | DD2091E224A645F800309C06 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | DD2091E324A645F800309C06 /* VideoMeme.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VideoMeme.entitlements; sourceTree = ""; }; 29 | DD2091EA24A6487500309C06 /* VideoRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoRenderer.swift; sourceTree = ""; }; 30 | DD2091EB24A6487500309C06 /* VideoComposition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoComposition.swift; sourceTree = ""; }; 31 | DD2091EE24A648D000309C06 /* VideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoViewModel.swift; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | DD2091D124A645F200309C06 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | DD2091CB24A645F200309C06 = { 46 | isa = PBXGroup; 47 | children = ( 48 | DD2091D624A645F200309C06 /* VideoMeme */, 49 | DD2091D524A645F200309C06 /* Products */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | DD2091D524A645F200309C06 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | DD2091D424A645F200309C06 /* VideoMeme.app */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | DD2091D624A645F200309C06 /* VideoMeme */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | DD2091E924A6486200309C06 /* Rendering */, 65 | DD2091D724A645F200309C06 /* VideoMemeApp.swift */, 66 | DD2091D924A645F200309C06 /* VideoMemeDocument.swift */, 67 | DD2091EE24A648D000309C06 /* VideoViewModel.swift */, 68 | DD2091DB24A645F200309C06 /* ContentView.swift */, 69 | DD2091DD24A645F800309C06 /* Assets.xcassets */, 70 | DD2091E224A645F800309C06 /* Info.plist */, 71 | DD2091E324A645F800309C06 /* VideoMeme.entitlements */, 72 | DD2091DF24A645F800309C06 /* Preview Content */, 73 | ); 74 | path = VideoMeme; 75 | sourceTree = ""; 76 | }; 77 | DD2091DF24A645F800309C06 /* Preview Content */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | DD2091E024A645F800309C06 /* Preview Assets.xcassets */, 81 | ); 82 | path = "Preview Content"; 83 | sourceTree = ""; 84 | }; 85 | DD2091E924A6486200309C06 /* Rendering */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | DD2091EB24A6487500309C06 /* VideoComposition.swift */, 89 | DD2091EA24A6487500309C06 /* VideoRenderer.swift */, 90 | ); 91 | path = Rendering; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | DD2091D324A645F200309C06 /* VideoMeme */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = DD2091E624A645F800309C06 /* Build configuration list for PBXNativeTarget "VideoMeme" */; 100 | buildPhases = ( 101 | DD2091D024A645F200309C06 /* Sources */, 102 | DD2091D124A645F200309C06 /* Frameworks */, 103 | DD2091D224A645F200309C06 /* Resources */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = VideoMeme; 110 | productName = VideoMeme; 111 | productReference = DD2091D424A645F200309C06 /* VideoMeme.app */; 112 | productType = "com.apple.product-type.application"; 113 | }; 114 | /* End PBXNativeTarget section */ 115 | 116 | /* Begin PBXProject section */ 117 | DD2091CC24A645F200309C06 /* Project object */ = { 118 | isa = PBXProject; 119 | attributes = { 120 | LastSwiftUpdateCheck = 1200; 121 | LastUpgradeCheck = 1200; 122 | TargetAttributes = { 123 | DD2091D324A645F200309C06 = { 124 | CreatedOnToolsVersion = 12.0; 125 | }; 126 | }; 127 | }; 128 | buildConfigurationList = DD2091CF24A645F200309C06 /* Build configuration list for PBXProject "VideoMeme" */; 129 | compatibilityVersion = "Xcode 9.3"; 130 | developmentRegion = en; 131 | hasScannedForEncodings = 0; 132 | knownRegions = ( 133 | en, 134 | Base, 135 | ); 136 | mainGroup = DD2091CB24A645F200309C06; 137 | productRefGroup = DD2091D524A645F200309C06 /* Products */; 138 | projectDirPath = ""; 139 | projectRoot = ""; 140 | targets = ( 141 | DD2091D324A645F200309C06 /* VideoMeme */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | DD2091D224A645F200309C06 /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | DD2091E124A645F800309C06 /* Preview Assets.xcassets in Resources */, 152 | DD2091DE24A645F800309C06 /* Assets.xcassets in Resources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXSourcesBuildPhase section */ 159 | DD2091D024A645F200309C06 /* Sources */ = { 160 | isa = PBXSourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | DD2091ED24A6487500309C06 /* VideoComposition.swift in Sources */, 164 | DD2091EC24A6487500309C06 /* VideoRenderer.swift in Sources */, 165 | DD2091DA24A645F200309C06 /* VideoMemeDocument.swift in Sources */, 166 | DD2091D824A645F200309C06 /* VideoMemeApp.swift in Sources */, 167 | DD2091EF24A648D000309C06 /* VideoViewModel.swift in Sources */, 168 | DD2091DC24A645F200309C06 /* ContentView.swift in Sources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXSourcesBuildPhase section */ 173 | 174 | /* Begin XCBuildConfiguration section */ 175 | DD2091E424A645F800309C06 /* Debug */ = { 176 | isa = XCBuildConfiguration; 177 | buildSettings = { 178 | ALWAYS_SEARCH_USER_PATHS = NO; 179 | CLANG_ANALYZER_NONNULL = YES; 180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 182 | CLANG_CXX_LIBRARY = "libc++"; 183 | CLANG_ENABLE_MODULES = YES; 184 | CLANG_ENABLE_OBJC_ARC = YES; 185 | CLANG_ENABLE_OBJC_WEAK = YES; 186 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 187 | CLANG_WARN_BOOL_CONVERSION = YES; 188 | CLANG_WARN_COMMA = YES; 189 | CLANG_WARN_CONSTANT_CONVERSION = YES; 190 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 191 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 192 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INFINITE_RECURSION = YES; 196 | CLANG_WARN_INT_CONVERSION = YES; 197 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 198 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 199 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 201 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 202 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 203 | CLANG_WARN_STRICT_PROTOTYPES = YES; 204 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 205 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 206 | CLANG_WARN_UNREACHABLE_CODE = YES; 207 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 208 | COPY_PHASE_STRIP = NO; 209 | DEBUG_INFORMATION_FORMAT = dwarf; 210 | ENABLE_STRICT_OBJC_MSGSEND = YES; 211 | ENABLE_TESTABILITY = YES; 212 | GCC_C_LANGUAGE_STANDARD = gnu11; 213 | GCC_DYNAMIC_NO_PIC = NO; 214 | GCC_NO_COMMON_BLOCKS = YES; 215 | GCC_OPTIMIZATION_LEVEL = 0; 216 | GCC_PREPROCESSOR_DEFINITIONS = ( 217 | "DEBUG=1", 218 | "$(inherited)", 219 | ); 220 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 221 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 222 | GCC_WARN_UNDECLARED_SELECTOR = YES; 223 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 224 | GCC_WARN_UNUSED_FUNCTION = YES; 225 | GCC_WARN_UNUSED_VARIABLE = YES; 226 | MACOSX_DEPLOYMENT_TARGET = 10.16; 227 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 228 | MTL_FAST_MATH = YES; 229 | ONLY_ACTIVE_ARCH = YES; 230 | SDKROOT = macosx; 231 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 232 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 233 | }; 234 | name = Debug; 235 | }; 236 | DD2091E524A645F800309C06 /* Release */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_ENABLE_OBJC_WEAK = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INFINITE_RECURSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 271 | ENABLE_NS_ASSERTIONS = NO; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_NO_COMMON_BLOCKS = YES; 275 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 276 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 277 | GCC_WARN_UNDECLARED_SELECTOR = YES; 278 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 279 | GCC_WARN_UNUSED_FUNCTION = YES; 280 | GCC_WARN_UNUSED_VARIABLE = YES; 281 | MACOSX_DEPLOYMENT_TARGET = 10.16; 282 | MTL_ENABLE_DEBUG_INFO = NO; 283 | MTL_FAST_MATH = YES; 284 | SDKROOT = macosx; 285 | SWIFT_COMPILATION_MODE = wholemodule; 286 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 287 | }; 288 | name = Release; 289 | }; 290 | DD2091E724A645F800309C06 /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 294 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 295 | CODE_SIGN_ENTITLEMENTS = VideoMeme/VideoMeme.entitlements; 296 | CODE_SIGN_STYLE = Automatic; 297 | COMBINE_HIDPI_IMAGES = YES; 298 | DEVELOPMENT_ASSET_PATHS = "\"VideoMeme/Preview Content\""; 299 | DEVELOPMENT_TEAM = 8C7439RJLG; 300 | ENABLE_HARDENED_RUNTIME = YES; 301 | ENABLE_PREVIEWS = YES; 302 | INFOPLIST_FILE = VideoMeme/Info.plist; 303 | LD_RUNPATH_SEARCH_PATHS = ( 304 | "$(inherited)", 305 | "@executable_path/../Frameworks", 306 | ); 307 | MACOSX_DEPLOYMENT_TARGET = 10.16; 308 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VideoMeme; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | SWIFT_VERSION = 5.0; 311 | }; 312 | name = Debug; 313 | }; 314 | DD2091E824A645F800309C06 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 318 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 319 | CODE_SIGN_ENTITLEMENTS = VideoMeme/VideoMeme.entitlements; 320 | CODE_SIGN_STYLE = Automatic; 321 | COMBINE_HIDPI_IMAGES = YES; 322 | DEVELOPMENT_ASSET_PATHS = "\"VideoMeme/Preview Content\""; 323 | DEVELOPMENT_TEAM = 8C7439RJLG; 324 | ENABLE_HARDENED_RUNTIME = YES; 325 | ENABLE_PREVIEWS = YES; 326 | INFOPLIST_FILE = VideoMeme/Info.plist; 327 | LD_RUNPATH_SEARCH_PATHS = ( 328 | "$(inherited)", 329 | "@executable_path/../Frameworks", 330 | ); 331 | MACOSX_DEPLOYMENT_TARGET = 10.16; 332 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VideoMeme; 333 | PRODUCT_NAME = "$(TARGET_NAME)"; 334 | SWIFT_VERSION = 5.0; 335 | }; 336 | name = Release; 337 | }; 338 | /* End XCBuildConfiguration section */ 339 | 340 | /* Begin XCConfigurationList section */ 341 | DD2091CF24A645F200309C06 /* Build configuration list for PBXProject "VideoMeme" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | DD2091E424A645F800309C06 /* Debug */, 345 | DD2091E524A645F800309C06 /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | DD2091E624A645F800309C06 /* Build configuration list for PBXNativeTarget "VideoMeme" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | DD2091E724A645F800309C06 /* Debug */, 354 | DD2091E824A645F800309C06 /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | /* End XCConfigurationList section */ 360 | }; 361 | rootObject = DD2091CC24A645F200309C06 /* Project object */; 362 | } 363 | -------------------------------------------------------------------------------- /VideoMeme/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /VideoMeme/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /VideoMeme/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VideoMeme/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // VideoMeme 4 | // 5 | // Created by Guilherme Rambo on 26/06/20. 6 | // 7 | 8 | import SwiftUI 9 | import AVKit 10 | 11 | struct ContentView: View { 12 | @Binding var document: VideoMemeDocument 13 | 14 | var body: some View { 15 | ZStack { 16 | VideoPlayer(player: document.viewModel.player) 17 | 18 | VStack { 19 | TextField("Text", text: $document.title) 20 | .foregroundColor(.white) 21 | .shadow(radius: 2) 22 | .font(.system(.largeTitle, design: .rounded)) 23 | .textFieldStyle(PlainTextFieldStyle()) 24 | .multilineTextAlignment(.center) 25 | .padding(.top, 60) 26 | .padding() 27 | 28 | Spacer() 29 | } 30 | } 31 | } 32 | } 33 | 34 | struct ContentView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | ContentView(document: .constant(VideoMemeDocument())) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /VideoMeme/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | Video files 12 | CFBundleTypeRole 13 | Viewer 14 | LSHandlerRank 15 | Default 16 | LSItemContentTypes 17 | 18 | public.mpeg-4 19 | 20 | NSUbiquitousDocumentUserActivityType 21 | $(PRODUCT_BUNDLE_IDENTIFIER).example-document 22 | 23 | 24 | CFBundleExecutable 25 | $(EXECUTABLE_NAME) 26 | CFBundleIdentifier 27 | $(PRODUCT_BUNDLE_IDENTIFIER) 28 | CFBundleInfoDictionaryVersion 29 | 6.0 30 | CFBundleName 31 | $(PRODUCT_NAME) 32 | CFBundlePackageType 33 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 34 | CFBundleShortVersionString 35 | 1.0 36 | CFBundleVersion 37 | 1 38 | LSMinimumSystemVersion 39 | $(MACOSX_DEPLOYMENT_TARGET) 40 | UTImportedTypeDeclarations 41 | 42 | 43 | UTTypeConformsTo 44 | 45 | public.movie 46 | 47 | UTTypeDescription 48 | Video files 49 | UTTypeIcons 50 | 51 | UTTypeIdentifier 52 | public.mpeg-4 53 | UTTypeTagSpecification 54 | 55 | public.filename-extension 56 | 57 | mp4 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /VideoMeme/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /VideoMeme/Rendering/VideoComposition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoComposition.swift 3 | // VideoMeme 4 | // 5 | // Created by Guilherme Rambo on 26/06/20. 6 | // 7 | 8 | import Cocoa 9 | import AVFoundation 10 | import CoreMedia 11 | 12 | final class VideoComposition: AVMutableComposition { 13 | 14 | let title: String 15 | let video: AVAsset 16 | 17 | var videoComposition: AVMutableVideoComposition? 18 | 19 | init(video: AVAsset, title: String) throws { 20 | self.video = video 21 | self.title = title 22 | 23 | super.init() 24 | 25 | guard let newVideoTrack = addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { 26 | // Should this ever fail in real life? Who knows... 27 | preconditionFailure("Failed to add video track to composition") 28 | } 29 | 30 | if let videoTrack = video.tracks(withMediaType: .video).first { 31 | try newVideoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: video.duration), of: videoTrack, at: .zero) 32 | } 33 | 34 | if let newAudioTrack = addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) { 35 | if let audioTrack = video.tracks(withMediaType: .audio).first { 36 | try newAudioTrack.insertTimeRange(CMTimeRange(start: .zero, duration: video.duration), of: audioTrack, at: .zero) 37 | } 38 | } 39 | 40 | configureComposition(videoTrack: newVideoTrack) 41 | } 42 | 43 | private func configureComposition(videoTrack: AVMutableCompositionTrack) { 44 | let mainInstruction = AVMutableVideoCompositionInstruction() 45 | mainInstruction.timeRange = CMTimeRange(start: .zero, duration: video.duration) 46 | 47 | let videolayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) 48 | mainInstruction.layerInstructions = [videolayerInstruction] 49 | 50 | videoComposition = AVMutableVideoComposition(propertiesOf: video) 51 | videoComposition?.instructions = [mainInstruction] 52 | 53 | composeText(with: videoTrack.naturalSize) 54 | } 55 | 56 | private func composeText(with renderSize: CGSize) { 57 | let titleLayer = CATextLayer() 58 | 59 | titleLayer.string = attributedTitle 60 | titleLayer.shadowColor = NSColor.black.cgColor 61 | titleLayer.shadowOpacity = 0.7 62 | titleLayer.shadowRadius = 1 63 | titleLayer.shadowOffset = CGSize(width: 0.5, height: 0.5) 64 | 65 | // Compute final title layer width based on title. 66 | 67 | let titleSize = attributedTitle.size() 68 | 69 | titleLayer.frame = CGRect( 70 | x: renderSize.width / 2 - titleSize.width / 2, 71 | y: 60, 72 | width: titleSize.width, 73 | height: titleSize.height 74 | ) 75 | 76 | let container = CALayer() 77 | container.frame = CGRect(origin: .zero, size: renderSize) 78 | 79 | let videoLayer = CALayer() 80 | videoLayer.frame = container.bounds 81 | container.addSublayer(videoLayer) 82 | 83 | container.addSublayer(titleLayer) 84 | 85 | container.isGeometryFlipped = true 86 | 87 | videoComposition?.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: container) 88 | 89 | titleLayer.minificationFilter = .trilinear 90 | 91 | /// Workaround rdar://32718905 92 | titleLayer.display() 93 | } 94 | 95 | private lazy var attributedTitle: NSAttributedString = { 96 | NSAttributedString.create( 97 | with: title, 98 | font: .roundedSystemFont(ofSize: 16, weight: .medium), 99 | color: .white 100 | ) 101 | }() 102 | 103 | } 104 | 105 | extension NSAttributedString { 106 | static func create(with string: String, 107 | font: NSFont, 108 | color: NSColor, 109 | lineHeightMultiple: CGFloat = 1, 110 | alignment: NSTextAlignment = .left, 111 | lineBreakMode: NSLineBreakMode = .byWordWrapping) -> NSAttributedString { 112 | let pStyle = NSMutableParagraphStyle() 113 | pStyle.lineHeightMultiple = lineHeightMultiple 114 | pStyle.alignment = alignment 115 | pStyle.lineBreakMode = lineBreakMode 116 | 117 | let attrs: [NSAttributedString.Key: Any] = [ 118 | .font: font, 119 | .foregroundColor: color, 120 | .paragraphStyle: pStyle 121 | ] 122 | 123 | return NSAttributedString(string: string, attributes: attrs) 124 | } 125 | 126 | } 127 | 128 | extension NSFont { 129 | 130 | static func roundedSystemFont(ofSize size: CGFloat, weight: NSFont.Weight = .regular) -> NSFont { 131 | guard let desc = NSFont.systemFont(ofSize: size, weight: weight).fontDescriptor.withDesign(.rounded) else { 132 | assertionFailure("Failed to get font descriptor") 133 | return NSFont.systemFont(ofSize: size, weight: weight) 134 | } 135 | 136 | return NSFont(descriptor: desc, size: size) ?? NSFont.systemFont(ofSize: size, weight: weight) 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /VideoMeme/Rendering/VideoRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoRenderer.swift 3 | // VideoMeme 4 | // 5 | // Created by Guilherme Rambo on 26/06/20. 6 | // 7 | 8 | import Cocoa 9 | import AVFoundation 10 | import os.log 11 | import Combine 12 | 13 | final class VideoRenderer: ObservableObject { 14 | 15 | init() { } 16 | 17 | private let log = OSLog(subsystem: "VideoText", category: String(describing: VideoRenderer.self)) 18 | 19 | private func error(with message: String) -> Error { 20 | NSError(domain: "codes.rambo.VideoRenderer", code: 0, userInfo: [NSLocalizedRecoverySuggestionErrorKey: message]) 21 | } 22 | 23 | typealias RenderProgressBlock = (Float) -> Void 24 | typealias RenderCompletionBlock = (Result) -> Void 25 | 26 | private var completionHandler: RenderCompletionBlock? 27 | 28 | private var currentSession: AVAssetExportSession? 29 | 30 | func renderClip(playerItem: AVPlayerItem, title: String, outputURL: URL, completion: @escaping RenderCompletionBlock) { 31 | os_log("%{public}@", log: log, type: .debug, #function) 32 | 33 | completionHandler = completion 34 | 35 | let preset = AVAssetExportPreset640x480 36 | 37 | do { 38 | let comp = try VideoComposition( 39 | video: playerItem.asset, 40 | title: title 41 | ) 42 | 43 | guard let session = AVAssetExportSession(asset: comp, presetName: preset) else { 44 | completion(.failure(error(with: "The export session couldn't be initialized."))) 45 | return 46 | } 47 | 48 | session.videoComposition = comp.videoComposition 49 | 50 | currentSession = session 51 | 52 | startExport(with: session, playerItem: playerItem, title: title, outputURL: outputURL) 53 | 54 | startProgressReporting() 55 | } catch { 56 | os_log("Composition initialization failed: %{public}@", log: self.log, type: .error, String(describing: error)) 57 | 58 | reportCompletion(with: .failure(self.error(with: "Couldn't create video composition for clip."))) 59 | } 60 | } 61 | 62 | private func startExport(with session: AVAssetExportSession, playerItem: AVPlayerItem, title: String, outputURL: URL) { 63 | session.outputFileType = .mp4 64 | session.outputURL = outputURL 65 | 66 | os_log("Will output to %@", log: self.log, type: .debug, outputURL.path) 67 | 68 | let startTime = playerItem.reversePlaybackEndTime 69 | let endTime = playerItem.forwardPlaybackEndTime 70 | let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime) 71 | session.timeRange = timeRange 72 | 73 | session.exportAsynchronously { [weak session, weak self] in 74 | guard let self = self else { return } 75 | guard let session = session else { return } 76 | 77 | switch session.status { 78 | case .unknown: 79 | os_log("Export session received unknown status") 80 | case .waiting: 81 | os_log("Export session waiting") 82 | case .exporting: 83 | os_log("Export session started") 84 | case .completed: 85 | os_log("Export session finished") 86 | self.progressUpdateTimer?.invalidate() 87 | 88 | self.reportCompletion(with: .success(outputURL)) 89 | case .failed: 90 | if let error = session.error { 91 | os_log("Export session failed with error: %{public}@", log: self.log, type: .error, String(describing: error)) 92 | } else { 93 | os_log("Export session failed with an unknown error", log: self.log, type: .error) 94 | } 95 | 96 | self.reportCompletion(with: .failure(self.error(with: "The export failed."))) 97 | case .cancelled: 98 | self.progressUpdateTimer?.invalidate() 99 | os_log("Cancelled", log: self.log, type: .debug) 100 | return 101 | @unknown default: 102 | fatalError("Unknown case") 103 | } 104 | } 105 | } 106 | 107 | private var progressUpdateTimer: Timer? 108 | 109 | @Published private(set) var currentProgress: Float = 0 110 | 111 | private func startProgressReporting() { 112 | let progressCheckInterval: TimeInterval = 0.1 113 | 114 | progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: progressCheckInterval, repeats: true, block: { [weak self] timer in 115 | guard let self = self else { return } 116 | 117 | guard self.currentSession?.status == .exporting else { 118 | timer.invalidate() 119 | self.progressUpdateTimer = nil 120 | return 121 | } 122 | 123 | DispatchQueue.main.async { 124 | self.currentProgress = self.currentSession?.progress ?? 0 125 | } 126 | }) 127 | } 128 | 129 | func cancel() { 130 | currentSession?.cancelExport() 131 | currentSession = nil 132 | } 133 | 134 | private func reportCompletion(with result: Result) { 135 | DispatchQueue.main.async { 136 | self.completionHandler?(result) 137 | } 138 | } 139 | 140 | } 141 | 142 | -------------------------------------------------------------------------------- /VideoMeme/VideoMeme.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /VideoMeme/VideoMemeApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoMemeApp.swift 3 | // VideoMeme 4 | // 5 | // Created by Guilherme Rambo on 26/06/20. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | @main 12 | struct VideoMemeApp: App { 13 | var body: some Scene { 14 | DocumentScene() 15 | } 16 | } 17 | 18 | struct DocumentScene: Scene { 19 | 20 | private let exportCommand = PassthroughSubject() 21 | 22 | var body: some Scene { 23 | DocumentGroup(viewing: VideoMemeDocument.self) { file in 24 | ContentView(document: file.$document) 25 | .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) 26 | .onReceive(exportCommand) { _ in 27 | file.document.beginExport() 28 | } 29 | }.commands { 30 | CommandMenu("Video") { 31 | Button("Export…") { 32 | exportCommand.send() 33 | } 34 | .keyboardShortcut("e", modifiers: .command) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /VideoMeme/VideoMemeDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoMemeDocument.swift 3 | // VideoMeme 4 | // 5 | // Created by Guilherme Rambo on 26/06/20. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | import AVFoundation 11 | 12 | extension UTType { 13 | static var videoFiles: UTType { 14 | UTType(importedAs: "public.mpeg-4") 15 | } 16 | } 17 | 18 | struct VideoMemeDocument: FileDocument { 19 | 20 | var renderer = VideoRenderer() 21 | 22 | let viewModel = VideoViewModel() 23 | 24 | var title = "Enter Title Here" 25 | 26 | static var readableContentTypes: [UTType] { [.videoFiles] } 27 | 28 | private let scratchURL: URL 29 | 30 | init() { scratchURL = Self.makeTemporaryFileURL() } 31 | 32 | init(fileWrapper: FileWrapper, contentType: UTType) throws { 33 | let tempURL = Self.makeTemporaryFileURL() 34 | self.scratchURL = tempURL 35 | 36 | guard let videoData = fileWrapper.regularFileContents else { return } 37 | 38 | FileManager.default.createFile(atPath: tempURL.path, contents: videoData, attributes: nil) 39 | 40 | viewModel.videoURL = tempURL 41 | } 42 | 43 | func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { 44 | let videoData = try Data(contentsOf: self.scratchURL) 45 | let wrapper = FileWrapper(regularFileWithContents: videoData) 46 | fileWrapper = wrapper 47 | } 48 | 49 | private static func makeTemporaryFileURL() -> URL { 50 | let name = UUID().uuidString 51 | return URL(fileURLWithPath: NSTemporaryDirectory()) 52 | .appendingPathComponent(name) 53 | .appendingPathExtension("mp4") 54 | } 55 | 56 | func beginExport() { 57 | let panel = NSSavePanel() 58 | panel.prompt = "Export" 59 | panel.allowedFileTypes = ["mp4"] 60 | 61 | guard panel.runModal() == .OK, let url = panel.url else { return } 62 | 63 | export(to: url) 64 | } 65 | 66 | private func export(to url: URL) { 67 | guard let item = viewModel.player.currentItem else { return } 68 | 69 | renderer.renderClip(playerItem: item, title: title, outputURL: url) { result in 70 | switch result { 71 | case .success: 72 | print("Done") 73 | case .failure(let error): 74 | print("Failed to render: \(error)") 75 | } 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /VideoMeme/VideoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoViewModel.swift 3 | // VideoMeme 4 | // 5 | // Created by Guilherme Rambo on 26/06/20. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | final class VideoViewModel: ObservableObject { 12 | 13 | var videoURL: URL? { 14 | didSet { 15 | guard let url = videoURL else { return } 16 | player = AVPlayer(url: url) 17 | } 18 | } 19 | 20 | @Published private(set) var player = AVPlayer() 21 | 22 | init() { } 23 | 24 | } 25 | --------------------------------------------------------------------------------