├── LivelyGIFs.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── XueYu.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── XueYu.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── LivelyGIFs.xcscheme │ └── xcschememanagement.plist ├── LivelyGIFs ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── GifExt.swift ├── Info.plist ├── LivePhotoVC.swift ├── PhotoCollectionVC.swift ├── PhotoCollectionViewCell.swift └── Regift.swift ├── ReadMe.md └── demo.gif /LivelyGIFs.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C80883491E940D1B00939B75 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80883481E940D1B00939B75 /* AppDelegate.swift */; }; 11 | C808834E1E940D1B00939B75 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C808834C1E940D1B00939B75 /* Main.storyboard */; }; 12 | C80883501E940D1B00939B75 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C808834F1E940D1B00939B75 /* Assets.xcassets */; }; 13 | C80883531E940D1B00939B75 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C80883511E940D1B00939B75 /* LaunchScreen.storyboard */; }; 14 | C808835B1E940D7200939B75 /* PhotoCollectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C808835A1E940D7200939B75 /* PhotoCollectionVC.swift */; }; 15 | C808835D1E94107000939B75 /* PhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C808835C1E94107000939B75 /* PhotoCollectionViewCell.swift */; }; 16 | C808835F1E94122D00939B75 /* LivePhotoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C808835E1E94122D00939B75 /* LivePhotoVC.swift */; }; 17 | C80883611E94272800939B75 /* Regift.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80883601E94272800939B75 /* Regift.swift */; }; 18 | C80883631E94277100939B75 /* GifExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80883621E94277100939B75 /* GifExt.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | C80883451E940D1B00939B75 /* LivelyGIFs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LivelyGIFs.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | C80883481E940D1B00939B75 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | C808834D1E940D1B00939B75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | C808834F1E940D1B00939B75 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | C80883521E940D1B00939B75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | C80883541E940D1B00939B75 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | C808835A1E940D7200939B75 /* PhotoCollectionVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionVC.swift; sourceTree = ""; }; 29 | C808835C1E94107000939B75 /* PhotoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewCell.swift; sourceTree = ""; }; 30 | C808835E1E94122D00939B75 /* LivePhotoVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivePhotoVC.swift; sourceTree = ""; }; 31 | C80883601E94272800939B75 /* Regift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Regift.swift; sourceTree = ""; }; 32 | C80883621E94277100939B75 /* GifExt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifExt.swift; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | C80883421E940D1B00939B75 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | C808833C1E940D1B00939B75 = { 47 | isa = PBXGroup; 48 | children = ( 49 | C80883471E940D1B00939B75 /* LivelyGIFs */, 50 | C80883461E940D1B00939B75 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | C80883461E940D1B00939B75 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | C80883451E940D1B00939B75 /* LivelyGIFs.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | C80883471E940D1B00939B75 /* LivelyGIFs */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | C808835A1E940D7200939B75 /* PhotoCollectionVC.swift */, 66 | C808835C1E94107000939B75 /* PhotoCollectionViewCell.swift */, 67 | C808835E1E94122D00939B75 /* LivePhotoVC.swift */, 68 | C80883481E940D1B00939B75 /* AppDelegate.swift */, 69 | C80883641E946DCC00939B75 /* Utilities */, 70 | C808834C1E940D1B00939B75 /* Main.storyboard */, 71 | C808834F1E940D1B00939B75 /* Assets.xcassets */, 72 | C80883511E940D1B00939B75 /* LaunchScreen.storyboard */, 73 | C80883541E940D1B00939B75 /* Info.plist */, 74 | ); 75 | path = LivelyGIFs; 76 | sourceTree = ""; 77 | }; 78 | C80883641E946DCC00939B75 /* Utilities */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | C80883601E94272800939B75 /* Regift.swift */, 82 | C80883621E94277100939B75 /* GifExt.swift */, 83 | ); 84 | name = Utilities; 85 | sourceTree = ""; 86 | }; 87 | /* End PBXGroup section */ 88 | 89 | /* Begin PBXNativeTarget section */ 90 | C80883441E940D1B00939B75 /* LivelyGIFs */ = { 91 | isa = PBXNativeTarget; 92 | buildConfigurationList = C80883571E940D1B00939B75 /* Build configuration list for PBXNativeTarget "LivelyGIFs" */; 93 | buildPhases = ( 94 | C80883411E940D1B00939B75 /* Sources */, 95 | C80883421E940D1B00939B75 /* Frameworks */, 96 | C80883431E940D1B00939B75 /* Resources */, 97 | ); 98 | buildRules = ( 99 | ); 100 | dependencies = ( 101 | ); 102 | name = LivelyGIFs; 103 | productName = LivelyGIFs; 104 | productReference = C80883451E940D1B00939B75 /* LivelyGIFs.app */; 105 | productType = "com.apple.product-type.application"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | C808833D1E940D1B00939B75 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | LastSwiftUpdateCheck = 0830; 114 | LastUpgradeCheck = 0830; 115 | ORGANIZATIONNAME = XueYu; 116 | TargetAttributes = { 117 | C80883441E940D1B00939B75 = { 118 | CreatedOnToolsVersion = 8.3; 119 | DevelopmentTeam = L2UHX34QKF; 120 | ProvisioningStyle = Automatic; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = C80883401E940D1B00939B75 /* Build configuration list for PBXProject "LivelyGIFs" */; 125 | compatibilityVersion = "Xcode 3.2"; 126 | developmentRegion = English; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ); 132 | mainGroup = C808833C1E940D1B00939B75; 133 | productRefGroup = C80883461E940D1B00939B75 /* Products */; 134 | projectDirPath = ""; 135 | projectRoot = ""; 136 | targets = ( 137 | C80883441E940D1B00939B75 /* LivelyGIFs */, 138 | ); 139 | }; 140 | /* End PBXProject section */ 141 | 142 | /* Begin PBXResourcesBuildPhase section */ 143 | C80883431E940D1B00939B75 /* Resources */ = { 144 | isa = PBXResourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | C80883531E940D1B00939B75 /* LaunchScreen.storyboard in Resources */, 148 | C80883501E940D1B00939B75 /* Assets.xcassets in Resources */, 149 | C808834E1E940D1B00939B75 /* Main.storyboard in Resources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXResourcesBuildPhase section */ 154 | 155 | /* Begin PBXSourcesBuildPhase section */ 156 | C80883411E940D1B00939B75 /* Sources */ = { 157 | isa = PBXSourcesBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | C80883491E940D1B00939B75 /* AppDelegate.swift in Sources */, 161 | C80883611E94272800939B75 /* Regift.swift in Sources */, 162 | C808835B1E940D7200939B75 /* PhotoCollectionVC.swift in Sources */, 163 | C808835D1E94107000939B75 /* PhotoCollectionViewCell.swift in Sources */, 164 | C80883631E94277100939B75 /* GifExt.swift in Sources */, 165 | C808835F1E94122D00939B75 /* LivePhotoVC.swift in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin PBXVariantGroup section */ 172 | C808834C1E940D1B00939B75 /* Main.storyboard */ = { 173 | isa = PBXVariantGroup; 174 | children = ( 175 | C808834D1E940D1B00939B75 /* Base */, 176 | ); 177 | name = Main.storyboard; 178 | sourceTree = ""; 179 | }; 180 | C80883511E940D1B00939B75 /* LaunchScreen.storyboard */ = { 181 | isa = PBXVariantGroup; 182 | children = ( 183 | C80883521E940D1B00939B75 /* Base */, 184 | ); 185 | name = LaunchScreen.storyboard; 186 | sourceTree = ""; 187 | }; 188 | /* End PBXVariantGroup section */ 189 | 190 | /* Begin XCBuildConfiguration section */ 191 | C80883551E940D1B00939B75 /* Debug */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | ALWAYS_SEARCH_USER_PATHS = NO; 195 | CLANG_ANALYZER_NONNULL = YES; 196 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 197 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 198 | CLANG_CXX_LIBRARY = "libc++"; 199 | CLANG_ENABLE_MODULES = YES; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_WARN_BOOL_CONVERSION = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 204 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 205 | CLANG_WARN_EMPTY_BODY = YES; 206 | CLANG_WARN_ENUM_CONVERSION = YES; 207 | CLANG_WARN_INFINITE_RECURSION = YES; 208 | CLANG_WARN_INT_CONVERSION = YES; 209 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 210 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 214 | COPY_PHASE_STRIP = NO; 215 | DEBUG_INFORMATION_FORMAT = dwarf; 216 | ENABLE_STRICT_OBJC_MSGSEND = YES; 217 | ENABLE_TESTABILITY = YES; 218 | GCC_C_LANGUAGE_STANDARD = gnu99; 219 | GCC_DYNAMIC_NO_PIC = NO; 220 | GCC_NO_COMMON_BLOCKS = YES; 221 | GCC_OPTIMIZATION_LEVEL = 0; 222 | GCC_PREPROCESSOR_DEFINITIONS = ( 223 | "DEBUG=1", 224 | "$(inherited)", 225 | ); 226 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 227 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 228 | GCC_WARN_UNDECLARED_SELECTOR = YES; 229 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 230 | GCC_WARN_UNUSED_FUNCTION = YES; 231 | GCC_WARN_UNUSED_VARIABLE = YES; 232 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 233 | MTL_ENABLE_DEBUG_INFO = YES; 234 | ONLY_ACTIVE_ARCH = YES; 235 | SDKROOT = iphoneos; 236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 238 | TARGETED_DEVICE_FAMILY = "1,2"; 239 | }; 240 | name = Debug; 241 | }; 242 | C80883561E940D1B00939B75 /* Release */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_NONNULL = YES; 247 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_WARN_BOOL_CONVERSION = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 255 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 256 | CLANG_WARN_EMPTY_BODY = YES; 257 | CLANG_WARN_ENUM_CONVERSION = YES; 258 | CLANG_WARN_INFINITE_RECURSION = YES; 259 | CLANG_WARN_INT_CONVERSION = YES; 260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 267 | ENABLE_NS_ASSERTIONS = NO; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | GCC_C_LANGUAGE_STANDARD = gnu99; 270 | GCC_NO_COMMON_BLOCKS = YES; 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 278 | MTL_ENABLE_DEBUG_INFO = NO; 279 | SDKROOT = iphoneos; 280 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 281 | TARGETED_DEVICE_FAMILY = "1,2"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Release; 285 | }; 286 | C80883581E940D1B00939B75 /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 290 | DEVELOPMENT_TEAM = L2UHX34QKF; 291 | INFOPLIST_FILE = LivelyGIFs/Info.plist; 292 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 293 | PRODUCT_BUNDLE_IDENTIFIER = com.xueyu.LivelyGIFs; 294 | PRODUCT_NAME = "$(TARGET_NAME)"; 295 | SWIFT_VERSION = 3.0; 296 | }; 297 | name = Debug; 298 | }; 299 | C80883591E940D1B00939B75 /* Release */ = { 300 | isa = XCBuildConfiguration; 301 | buildSettings = { 302 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 303 | DEVELOPMENT_TEAM = L2UHX34QKF; 304 | INFOPLIST_FILE = LivelyGIFs/Info.plist; 305 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 306 | PRODUCT_BUNDLE_IDENTIFIER = com.xueyu.LivelyGIFs; 307 | PRODUCT_NAME = "$(TARGET_NAME)"; 308 | SWIFT_VERSION = 3.0; 309 | }; 310 | name = Release; 311 | }; 312 | /* End XCBuildConfiguration section */ 313 | 314 | /* Begin XCConfigurationList section */ 315 | C80883401E940D1B00939B75 /* Build configuration list for PBXProject "LivelyGIFs" */ = { 316 | isa = XCConfigurationList; 317 | buildConfigurations = ( 318 | C80883551E940D1B00939B75 /* Debug */, 319 | C80883561E940D1B00939B75 /* Release */, 320 | ); 321 | defaultConfigurationIsVisible = 0; 322 | defaultConfigurationName = Release; 323 | }; 324 | C80883571E940D1B00939B75 /* Build configuration list for PBXNativeTarget "LivelyGIFs" */ = { 325 | isa = XCConfigurationList; 326 | buildConfigurations = ( 327 | C80883581E940D1B00939B75 /* Debug */, 328 | C80883591E940D1B00939B75 /* Release */, 329 | ); 330 | defaultConfigurationIsVisible = 0; 331 | defaultConfigurationName = Release; 332 | }; 333 | /* End XCConfigurationList section */ 334 | }; 335 | rootObject = C808833D1E940D1B00939B75 /* Project object */; 336 | } 337 | -------------------------------------------------------------------------------- /LivelyGIFs.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LivelyGIFs.xcodeproj/project.xcworkspace/xcuserdata/XueYu.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrisYu/LivelyGIFs/5fca1880d7e41050acbd7997b11de6bae4ba3880/LivelyGIFs.xcodeproj/project.xcworkspace/xcuserdata/XueYu.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /LivelyGIFs.xcodeproj/xcuserdata/XueYu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /LivelyGIFs.xcodeproj/xcuserdata/XueYu.xcuserdatad/xcschemes/LivelyGIFs.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /LivelyGIFs.xcodeproj/xcuserdata/XueYu.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LivelyGIFs.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | C80883441E940D1B00939B75 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LivelyGIFs/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LivelyGIFs 4 | // 5 | // Created by Xue Yu on 4/4/17. 6 | // Copyright © 2017 XueYu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /LivelyGIFs/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /LivelyGIFs/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LivelyGIFs/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /LivelyGIFs/GifExt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GifExt.swift 3 | // GifExtDemo 4 | // 5 | // Created by Xue Yu on 4/2/17. 6 | // Copyright © 2017 XueYu. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import ImageIO 12 | import MobileCoreServices 13 | 14 | extension UIImageView{ 15 | 16 | public func loadGif(data: Data){ 17 | DispatchQueue.global().async { 18 | let image = UIImage.gif(data: data) 19 | DispatchQueue.main.async { 20 | self.image = image 21 | } 22 | } 23 | } 24 | 25 | public func loadGif(url: URL){ 26 | DispatchQueue.global().async { 27 | let image = UIImage.gif(url: url) 28 | DispatchQueue.main.async { 29 | self.image = image 30 | } 31 | } 32 | } 33 | 34 | } 35 | 36 | extension UIImage{ 37 | 38 | public class func gif(data: Data) -> UIImage?{ 39 | // Create source from data 40 | guard let source = CGImageSourceCreateWithData(data as CFData, nil) else{ 41 | print("GifExt: Source for the image doesnot exist") 42 | return nil 43 | } 44 | 45 | return UIImage.animatedImageWithSource(source) 46 | } 47 | 48 | public class func gif(url: URL) -> UIImage?{ 49 | guard let imageData = try? Data(contentsOf: url) else { 50 | print("GifExt: Cannot turn image named \"\(url)\" into NSData") 51 | return nil 52 | } 53 | return gif(data: imageData) 54 | } 55 | 56 | internal class func animatedImageWithSource(_ source: CGImageSource) -> UIImage?{ 57 | 58 | // kCGImageSourceShouldCache: decode or not when storing 59 | // kCGImageSourceTypeIdentifierHint: source type 60 | 61 | let options: NSDictionary = [kCGImageSourceShouldCache as String: NSNumber(value: true), kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF] 62 | 63 | let frameCount = CGImageSourceGetCount(source) 64 | var images = [UIImage]() 65 | var gifDuration = 0.0 66 | 67 | for i in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | NSPhotoLibraryUsageDescription 45 | access live photos 46 | 47 | 48 | -------------------------------------------------------------------------------- /LivelyGIFs/LivePhotoVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LivePhotoVC.swift 3 | // LivelyGIFs 4 | // 5 | // Created by Xue Yu on 4/4/17. 6 | // Copyright © 2017 XueYu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | import PhotosUI 12 | import MobileCoreServices 13 | 14 | class LivePhotoVC: UIViewController { 15 | 16 | var livePhotoAsset: PHAsset? 17 | var photoView: PHLivePhotoView! 18 | var gifView: UIImageView! 19 | var gifURL: URL? 20 | @IBOutlet weak var exportShareButton: UIButton! 21 | @IBOutlet weak var gifSizeSegmentedControl: UISegmentedControl! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | gifSizeSegmentedControl.selectedSegmentIndex = 1 27 | 28 | photoView = PHLivePhotoView(frame: CGRect.init(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)) 29 | photoView.contentMode = .scaleAspectFit 30 | 31 | self.view.addSubview(photoView) 32 | 33 | gifView = UIImageView(frame: CGRect.init(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)) 34 | gifView.contentMode = .scaleAspectFit 35 | 36 | } 37 | 38 | 39 | override func viewDidLayoutSubviews() { 40 | super.viewDidLayoutSubviews() 41 | self.photoView.center = self.view.center 42 | self.gifView.center = self.view.center 43 | } 44 | 45 | 46 | override func viewWillAppear(_ animated: Bool) { 47 | super.viewWillAppear(animated) 48 | configureView() 49 | } 50 | 51 | func configureView() { 52 | if let photoAsset = livePhotoAsset { 53 | PHImageManager.default().requestLivePhoto(for: photoAsset, targetSize: photoView.frame.size, contentMode: .aspectFit, options: nil, resultHandler: { (photo: PHLivePhoto?, info: [AnyHashable : Any]?) in 54 | 55 | if let livePhoto = photo{ 56 | self.photoView.livePhoto = livePhoto 57 | self.photoView.startPlayback(with: .hint) 58 | 59 | if let photoLocation = photoAsset.location { 60 | let geoCoder = CLGeocoder() 61 | geoCoder.reverseGeocodeLocation(photoLocation, completionHandler: { (placemark: [CLPlacemark]?, error: Error?) in 62 | if error == nil { 63 | self.navigationItem.title = placemark?.first?.locality 64 | } 65 | }) 66 | } 67 | } 68 | }) 69 | } 70 | } 71 | 72 | 73 | @IBAction func segmentedControlClicked(_ sender: UISegmentedControl) { 74 | exportShareButton.setTitle("Export GIF", for: .normal) 75 | } 76 | 77 | 78 | 79 | @IBAction func exportShareButton(_ sender: UIButton) { 80 | if exportShareButton.titleLabel?.text == "Export GIF" { 81 | 82 | let resources = PHAssetResource.assetResources(for: livePhotoAsset!) 83 | for resource in resources { 84 | if resource.type == .pairedVideo { 85 | self.getMovieData(resource) 86 | break 87 | } 88 | } 89 | } else { 90 | let activityVC = UIActivityViewController(activityItems: [gifURL!], applicationActivities: nil) 91 | activityVC.popoverPresentationController?.sourceView = self.view 92 | self.present(activityVC, animated: true, completion: nil) 93 | } 94 | 95 | } 96 | 97 | 98 | func getMovieData(_ resource: PHAssetResource){ 99 | 100 | let movieURL = URL(fileURLWithPath: (NSTemporaryDirectory()).appending("video.mov")) 101 | removeFileIfExists(fileURL: movieURL) 102 | 103 | 104 | PHAssetResourceManager.default().writeData(for: resource, toFile: movieURL as URL, options: nil) { (error) in 105 | if error != nil{ 106 | print("Could not write video file") 107 | } else { 108 | self.convertToGIF(movieURL) 109 | } 110 | } 111 | } 112 | 113 | 114 | func convertToGIF(_ movieURL: URL){ 115 | 116 | let movieAsset = AVURLAsset(url: movieURL as URL) 117 | 118 | // collect the needed parameters 119 | let duration = CMTimeGetSeconds(movieAsset.duration) 120 | let track = movieAsset.tracks(withMediaType: AVMediaTypeVideo).first! 121 | let frameRate = track.nominalFrameRate 122 | 123 | gifURL = URL(fileURLWithPath: (NSTemporaryDirectory()).appending("file.gif")) 124 | removeFileIfExists(fileURL: gifURL!) 125 | 126 | var width = 0 127 | 128 | switch gifSizeSegmentedControl.selectedSegmentIndex { 129 | case 0: 130 | width = 240 131 | case 1: 132 | width = 480 133 | case 2: 134 | width = 640 135 | default: 136 | width = 0 137 | } 138 | 139 | 140 | Regift.createGIFFromSource(movieURL as URL, destinationFileURL: gifURL, startTime: 0.0, duration: Float(duration), frameRate: Int(frameRate), loopCount: 0, width: width, height: width) {_ in 141 | 142 | exportShareButton.setTitle("Share", for: .normal) 143 | self.gifView.loadGif(url: gifURL!) 144 | self.photoView.removeFromSuperview() 145 | self.view.addSubview(gifView) 146 | } 147 | 148 | } 149 | 150 | 151 | 152 | func removeFileIfExists(fileURL : URL) { 153 | let fileManager = FileManager.default 154 | if fileManager.fileExists(atPath: fileURL.path) { 155 | do { 156 | try fileManager.removeItem(at: fileURL) 157 | } 158 | catch { 159 | print("Could not delete exist file so cannot write to it") 160 | } 161 | } 162 | } 163 | 164 | 165 | } 166 | -------------------------------------------------------------------------------- /LivelyGIFs/PhotoCollectionVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoCollectionVC.swift 3 | // LivelyGIFs 4 | // 5 | // Created by Xue Yu on 4/4/17. 6 | // Copyright © 2017 XueYu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | private let reuseIdentifier = "Cell" 13 | 14 | class PhotoCollectionVC: UICollectionViewController { 15 | 16 | var livePhotoAssets: PHFetchResult? 17 | 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | 23 | PHPhotoLibrary.requestAuthorization { (status:PHAuthorizationStatus) in 24 | switch status { 25 | case .authorized: 26 | self.fetchPhotos() 27 | default: 28 | self.showNoPhotoAccessAlert() 29 | } 30 | } 31 | 32 | } 33 | 34 | 35 | 36 | func fetchPhotos() { 37 | 38 | let sortDesciptor = NSSortDescriptor(key: "creationDate", ascending:false) 39 | let predicate = NSPredicate(format: "(mediaSubtype & %d) != 0", PHAssetMediaSubtype.photoLive.rawValue) 40 | 41 | let options = PHFetchOptions() 42 | 43 | options.sortDescriptors = [sortDesciptor] 44 | options.predicate = predicate 45 | 46 | 47 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { 48 | self.livePhotoAssets = PHAsset.fetchAssets(with: options) 49 | 50 | DispatchQueue.main.async { 51 | self.collectionView?.reloadData() 52 | } 53 | } 54 | } 55 | 56 | 57 | func showNoPhotoAccessAlert() { 58 | let alert = UIAlertController(title: "No Photo Access Permission", message: "Please grant this App access your photos in Settings -- > Privacy", preferredStyle: .alert) 59 | 60 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 61 | alert.addAction(UIAlertAction(title: "Settings", style: .default, handler:{ (action: UIAlertAction) in 62 | let url = URL(string: UIApplicationOpenSettingsURLString) 63 | UIApplication.shared.open(url!, options: ["" : ""], completionHandler: nil) 64 | return 65 | })) 66 | 67 | self.present(alert, animated: true, completion: nil) 68 | } 69 | 70 | 71 | // MARK: UICollectionViewDataSource 72 | 73 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 74 | return 1 75 | } 76 | 77 | 78 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 79 | if let numberOfItems = livePhotoAssets?.count { 80 | return numberOfItems 81 | } else { 82 | return 0 83 | } 84 | } 85 | 86 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 87 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PhotoCollectionViewCell 88 | 89 | if let asset = livePhotoAssets?[indexPath.row]{ 90 | let options = PHImageRequestOptions() 91 | options.isNetworkAccessAllowed = true 92 | 93 | let targetSize = CGSize(width: 100, height: 100) 94 | PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options, resultHandler: { (image: UIImage?, info: [AnyHashable : Any]?) in 95 | cell.photoImageView.image = image 96 | }) 97 | } 98 | 99 | return cell 100 | } 101 | 102 | 103 | 104 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 105 | if let indexPath = collectionView?.indexPathsForSelectedItems?.first { 106 | let photoVC = segue.destination as! LivePhotoVC 107 | photoVC.livePhotoAsset = livePhotoAssets?[indexPath.item] 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /LivelyGIFs/PhotoCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoCollectionViewCell.swift 3 | // LivelyGIFs 4 | // 5 | // Created by Xue Yu on 4/4/17. 6 | // Copyright © 2017 XueYu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PhotoCollectionViewCell: UICollectionViewCell { 12 | 13 | @IBOutlet weak var photoImageView: UIImageView! 14 | 15 | } 16 | -------------------------------------------------------------------------------- /LivelyGIFs/Regift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Regift.swift 3 | // Regift 4 | // 5 | // Created by Matthew Palmer on 27/12/2014. 6 | // Copyright (c) 2014 Matthew Palmer. All rights reserved. 7 | // 8 | // Minor changes made by Xue Yu, add parameters for the gif width/height 9 | // 10 | 11 | #if os(iOS) 12 | import UIKit 13 | import MobileCoreServices 14 | #elseif os(OSX) 15 | import AppKit 16 | #endif 17 | 18 | import ImageIO 19 | import AVFoundation 20 | 21 | public typealias TimePoint = CMTime 22 | 23 | /// Errors thrown by Regift 24 | public enum RegiftError: String, Error { 25 | case DestinationNotFound = "The temp file destination could not be created or found" 26 | case SourceFormatInvalid = "The source file does not appear to be a valid format" 27 | case AddFrameToDestination = "An error occurred when adding a frame to the destination" 28 | case DestinationFinalize = "An error occurred when finalizing the destination" 29 | } 30 | 31 | // Convenience struct for managing dispatch groups. 32 | private struct Group { 33 | let group = DispatchGroup() 34 | func enter() { group.enter() } 35 | func leave() { group.leave() } 36 | func wait() { let _ = group.wait(timeout: DispatchTime.distantFuture) } 37 | } 38 | 39 | /// Easily convert a video to a GIF. It can convert the whole thing, or you can choose a section to trim out. 40 | /// 41 | /// Synchronous Usage: 42 | /// 43 | /// let regift = Regift(sourceFileURL: movieFileURL, frameCount: 24, delayTime: 0.5, loopCount: 7, width: 240, height: 240) 44 | /// print(regift.createGif()) 45 | /// 46 | /// // OR 47 | /// 48 | /// let trimmedRegift = Regift(sourceFileURL: movieFileURL, startTime: 30, duration: 15, frameRate: 15) 49 | /// print(trimmedRegift.createGif()) 50 | /// 51 | /// Asynchronous Usage: 52 | /// 53 | /// let regift = Regift.createGIFFromSource(movieFileURL, frameCount: 24, delayTime: 0.5, loopCount: 7) { (result) in 54 | /// print(result) 55 | /// } 56 | /// 57 | /// // OR 58 | /// 59 | /// let trimmedRegift = Regift.createGIFFromSource(movieFileURL, startTime: 30, duration: 15, frameRate: 15, loopCount: 0, width: 240, height: 240) { (result) in 60 | /// print(result) 61 | /// } 62 | /// 63 | public struct Regift { 64 | 65 | // Static conversion methods, for convenient and easy-to-use API: 66 | 67 | /** 68 | Create a GIF from a movie stored at the given URL. This converts the whole video to a GIF meeting the requested output parameters. 69 | 70 | - parameters: 71 | - sourceFileURL: The source file to create the GIF from. 72 | - destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided. 73 | - frameCount: The number of frames to include in the gif; each frame has the same duration and is spaced evenly over the video. 74 | - delayTime: The amount of time each frame exists for in the GIF. 75 | - loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely. 76 | - width: The maximum width of generated GIF. This defaults to `0`, means not compressed. 77 | - height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio. 78 | - completion: A block that will be called when the GIF creation is completed. The `result` parameter provides the path to the file, or will be `nil` if there was an error. 79 | */ 80 | public static func createGIFFromSource( 81 | _ sourceFileURL: URL, 82 | destinationFileURL: URL? = nil, 83 | frameCount: Int, 84 | delayTime: Float, 85 | loopCount: Int = 0, 86 | width: Int = 0, 87 | height: Int = 0, 88 | completion: (_ result: URL?) -> Void) { 89 | let gift = Regift( 90 | sourceFileURL: sourceFileURL, 91 | destinationFileURL: destinationFileURL, 92 | frameCount: frameCount, 93 | delayTime: delayTime, 94 | loopCount: loopCount, 95 | width: width, 96 | height: height 97 | ) 98 | 99 | completion(gift.createGif()) 100 | } 101 | 102 | /** 103 | Create a GIF from a movie stored at the given URL. This allows you to choose a start time and duration in the source material that will be used to create the GIF which meets the output parameters. 104 | 105 | - parameters: 106 | - sourceFileURL: The source file to create the GIF from. 107 | - destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided. 108 | - startTime: The time in seconds in the source material at which you want the GIF to start. 109 | - duration: The duration in seconds that you want to pull from the source material. 110 | - frameRate: The desired frame rate of the outputted GIF. 111 | - loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely. 112 | - width: The maximum width of generated GIF. This defaults to `0`, means not compressed. 113 | - height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio. 114 | - completion: A block that will be called when the GIF creation is completed. The `result` parameter provides the path to the file, or will be `nil` if there was an error. 115 | */ 116 | public static func createGIFFromSource( 117 | _ sourceFileURL: URL, 118 | destinationFileURL: URL? = nil, 119 | startTime: Float, 120 | duration: Float, 121 | frameRate: Int, 122 | loopCount: Int = 0, 123 | width: Int = 0, 124 | height: Int = 0, 125 | completion: (_ result: URL?) -> Void) { 126 | let gift = Regift( 127 | sourceFileURL: sourceFileURL, 128 | destinationFileURL: destinationFileURL, 129 | startTime: startTime, 130 | duration: duration, 131 | frameRate: frameRate, 132 | loopCount: loopCount, 133 | width: width, 134 | height: height 135 | ) 136 | 137 | completion(gift.createGif()) 138 | } 139 | 140 | fileprivate struct Constants { 141 | static let FileName = "regift.gif" 142 | static let TimeInterval: Int32 = 600 143 | static let Tolerance = 0.01 144 | } 145 | 146 | /// A reference to the asset we are converting. 147 | fileprivate var asset: AVAsset 148 | 149 | /// The url for the source file. 150 | fileprivate let sourceFileURL: URL 151 | 152 | /// The point in time in the source which we will start from. 153 | fileprivate var startTime: Float = 0 154 | 155 | /// The desired duration of the gif. 156 | fileprivate var duration: Float 157 | 158 | /// The total length of the movie, in seconds. 159 | fileprivate var movieLength: Float 160 | 161 | /// The number of frames we are going to use to create the gif. 162 | fileprivate let frameCount: Int 163 | 164 | /// The amount of time each frame will remain on screen in the gif. 165 | fileprivate let delayTime: Float 166 | 167 | /// The number of times the gif will loop (0 is infinite). 168 | fileprivate let loopCount: Int 169 | 170 | /// The destination path for the generated file. 171 | fileprivate var destinationFileURL: URL? 172 | 173 | /// The maximum width/height for the generated file (0 will not compress).compress 174 | fileprivate var width: Int 175 | fileprivate var height: Int 176 | 177 | /** 178 | Create a GIF from a movie stored at the given URL. This converts the whole video to a GIF meeting the requested output parameters. 179 | 180 | - parameters: 181 | - sourceFileURL: The source file to create the GIF from. 182 | - destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided. 183 | - frameCount: The number of frames to include in the gif; each frame has the same duration and is spaced evenly over the video. 184 | - delayTime: The amount of time each frame exists for in the GIF. 185 | - loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely. 186 | - width: The maximum width of generated GIF. This defaults to `0`, means not compressed. 187 | - height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio. 188 | */ 189 | public init(sourceFileURL: URL, destinationFileURL: URL? = nil, frameCount: Int, delayTime: Float, loopCount: Int = 0, width: Int = 0, height: Int = 0) { 190 | self.sourceFileURL = sourceFileURL 191 | self.asset = AVURLAsset(url: sourceFileURL, options: nil) 192 | self.movieLength = Float(asset.duration.value) / Float(asset.duration.timescale) 193 | self.duration = movieLength 194 | self.delayTime = delayTime 195 | self.loopCount = loopCount 196 | self.destinationFileURL = destinationFileURL 197 | self.frameCount = frameCount 198 | self.width = width 199 | self.height = height 200 | } 201 | 202 | /** 203 | Create a GIF from a movie stored at the given URL. This allows you to choose a start time and duration in the source material that will be used to create the GIF which meets the output parameters. 204 | 205 | - parameters: 206 | - sourceFileURL: The source file to create the GIF from. 207 | - destinationFileURL: An optional destination file to write the GIF to. If you don't include this, a default path will be provided. 208 | - startTime: The time in seconds in the source material at which you want the GIF to start. 209 | - duration: The duration in seconds that you want to pull from the source material. 210 | - frameRate: The desired frame rate of the outputted GIF. 211 | - loopCount: The number of times the GIF will repeat. This defaults to `0`, which means that the GIF will repeat infinitely. 212 | - width: The maximum width of generated GIF. This defaults to `0`, means not compressed. 213 | - height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio. 214 | */ 215 | public init(sourceFileURL: URL, destinationFileURL: URL? = nil, startTime: Float, duration: Float, frameRate: Int, loopCount: Int = 0, width: Int = 0, height: Int = 0) { 216 | self.sourceFileURL = sourceFileURL 217 | self.asset = AVURLAsset(url: sourceFileURL, options: nil) 218 | self.destinationFileURL = destinationFileURL 219 | self.startTime = startTime 220 | self.duration = duration 221 | 222 | // The delay time is based on the desired framerate of the gif. 223 | self.delayTime = (1.0 / Float(frameRate)) 224 | 225 | // The frame count is based on the desired length and framerate of the gif. 226 | self.frameCount = Int(duration * Float(frameRate)) 227 | 228 | // The total length of the file, in seconds. 229 | self.movieLength = Float(asset.duration.value) / Float(asset.duration.timescale) 230 | 231 | self.loopCount = loopCount 232 | self.width = width 233 | self.height = height 234 | } 235 | 236 | /** 237 | Get the URL of the GIF created with the attributes provided in the initializer. 238 | 239 | - returns: The path to the created GIF, or `nil` if there was an error creating it. 240 | */ 241 | public func createGif() -> URL? { 242 | 243 | let fileProperties = [kCGImagePropertyGIFDictionary as String:[ 244 | kCGImagePropertyGIFLoopCount as String: NSNumber(value: Int32(loopCount) as Int32)], 245 | kCGImagePropertyGIFHasGlobalColorMap as String: NSValue(nonretainedObject: true) 246 | ] as [String : Any] 247 | 248 | let frameProperties = [ 249 | kCGImagePropertyGIFDictionary as String:[ 250 | kCGImagePropertyGIFDelayTime as String:delayTime 251 | ] 252 | ] 253 | 254 | // How far along the video track we want to move, in seconds. 255 | let increment = Float(duration) / Float(frameCount) 256 | 257 | // Add each of the frames to the buffer 258 | var timePoints: [TimePoint] = [] 259 | 260 | for frameNumber in 0 ..< frameCount { 261 | let seconds: Float64 = Float64(startTime) + (Float64(increment) * Float64(frameNumber)) 262 | let time = CMTimeMakeWithSeconds(seconds, Constants.TimeInterval) 263 | 264 | timePoints.append(time) 265 | } 266 | 267 | do { 268 | return try createGIFForTimePoints(timePoints, fileProperties: fileProperties as [String : AnyObject], frameProperties: frameProperties as [String : AnyObject], frameCount: frameCount, width: width, height: height) 269 | 270 | } catch { 271 | return nil 272 | } 273 | } 274 | 275 | /** 276 | Create a GIF using the given time points in a movie file stored in this Regift's `asset`. 277 | 278 | - parameters: 279 | - timePoints: timePoints An array of `TimePoint`s (which are typealiased `CMTime`s) to use as the frames in the GIF. 280 | - fileProperties: The desired attributes of the resulting GIF. 281 | - frameProperties: The desired attributes of each frame in the resulting GIF. 282 | - frameCount: The desired number of frames for the GIF. *NOTE: This seems redundant to me, as `timePoints.count` should really be what we are after, but I'm hesitant to change the API here.* 283 | - width: The maximum width of generated GIF. This defaults to `0`, means not compressed. 284 | - height: The maximum height of generated GIF. This defaults to `0`, means not compressed. Setting width/height will not change the image aspect ratio. 285 | 286 | - returns: The path to the created GIF, or `nil` if there was an error creating it. 287 | */ 288 | public func createGIFForTimePoints(_ timePoints: [TimePoint], fileProperties: [String: AnyObject], frameProperties: [String: AnyObject], frameCount: Int, width: Int, height: Int) throws -> URL { 289 | // Ensure the source media is a valid file. 290 | guard asset.tracks(withMediaCharacteristic: AVMediaCharacteristicVisual).count > 0 else { 291 | throw RegiftError.SourceFormatInvalid 292 | } 293 | 294 | var fileURL:URL? 295 | if self.destinationFileURL != nil { 296 | fileURL = self.destinationFileURL 297 | } else { 298 | let temporaryFile = (NSTemporaryDirectory() as NSString).appendingPathComponent(Constants.FileName) 299 | fileURL = URL(fileURLWithPath: temporaryFile) 300 | } 301 | 302 | guard let destination = CGImageDestinationCreateWithURL(fileURL! as CFURL, kUTTypeGIF, frameCount, nil) else { 303 | throw RegiftError.DestinationNotFound 304 | } 305 | 306 | CGImageDestinationSetProperties(destination, fileProperties as CFDictionary) 307 | 308 | let generator = AVAssetImageGenerator(asset: asset) 309 | 310 | generator.appliesPreferredTrackTransform = true 311 | generator.maximumSize = CGSize(width: width, height: height) 312 | 313 | let tolerance = CMTimeMakeWithSeconds(Constants.Tolerance, Constants.TimeInterval) 314 | generator.requestedTimeToleranceBefore = tolerance 315 | generator.requestedTimeToleranceAfter = tolerance 316 | 317 | // Transform timePoints to times for the async asset generator method. 318 | var times = [NSValue]() 319 | for time in timePoints { 320 | times.append(NSValue(time: time)) 321 | } 322 | 323 | // Create a dispatch group to force synchronous behavior on an asynchronous method. 324 | let gifGroup = Group() 325 | var dispatchError: Bool = false 326 | gifGroup.enter() 327 | 328 | generator.generateCGImagesAsynchronously(forTimes: times, completionHandler: { (requestedTime, image, actualTime, result, error) in 329 | guard let imageRef = image , error == nil else { 330 | print("An error occurred: \(error), image is \(image)") 331 | dispatchError = true 332 | gifGroup.leave() 333 | return 334 | } 335 | 336 | CGImageDestinationAddImage(destination, imageRef, frameProperties as CFDictionary) 337 | 338 | if requestedTime == times.last?.timeValue { 339 | gifGroup.leave() 340 | } 341 | }) 342 | 343 | // Wait for the asynchronous generator to finish. 344 | gifGroup.wait() 345 | 346 | // If there was an error in the generator, throw the error. 347 | if dispatchError { 348 | throw RegiftError.AddFrameToDestination 349 | } 350 | 351 | CGImageDestinationSetProperties(destination, fileProperties as CFDictionary) 352 | 353 | // Finalize the gif 354 | if !CGImageDestinationFinalize(destination) { 355 | throw RegiftError.DestinationFinalize 356 | } 357 | 358 | return fileURL! 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # LivelyGIFs 2 | 3 | Show your Live Photo and export as GIF. 4 | 5 | [![Swift 3.0](https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat)](https://developer.apple.com/swift/) [![License MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | 7 | 8 | Demo 9 | 10 | ![Demo](demo.gif) 11 | 12 | 13 | 14 | ## HighLights 15 | 16 | 17 | 18 | - Do not use Pod or Cathage to install 3rd party library 19 | - Simple logic, new learner friendly 20 | 21 | 22 | 23 | ## Known problems && Possible improvements 24 | 25 | 1. Load all live photos at once may cost too much. 26 | 2. Live photos assets are loaded once in viewDidLoad so may not show the dynamic changes. 27 | 3. Activity indicator to avoid white screen gap. 28 | 29 | 30 | ## Inspired by 31 | 32 | [LiveGIFs](https://github.com/neonichu/LiveGIFs) 33 | 34 | ## Thanks 35 | 36 | 37 | - [Regift](https://github.com/matthewpalmer/Regift) 38 | - [LivePreview](https://github.com/daver234/LivePreview) 39 | 40 | 41 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrisYu/LivelyGIFs/5fca1880d7e41050acbd7997b11de6bae4ba3880/demo.gif --------------------------------------------------------------------------------