├── .gitignore ├── README.md ├── LiveGIFs.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Podfile ├── LiveGIFs.xcworkspace └── contents.xcworkspacedata ├── Code ├── AppDelegate.swift ├── ImageCell.swift ├── GIFViewController.swift ├── ViewController.swift └── GIFication.swift ├── Resources ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json └── Base.lproj │ └── LaunchScreen.storyboard ├── LICENSE ├── Supporting Files └── Info.plist └── Podfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ## CocoaPods 2 | 3 | Pods 4 | 5 | ## Node 6 | 7 | node_modules 8 | 9 | ## OS X 10 | 11 | .DS_Store 12 | 13 | ## Other 14 | 15 | .gutter.json 16 | .pt 17 | 18 | ## Xcode 19 | 20 | *.xccheckout 21 | xcuserdata 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveGIFs 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | Free your Live Photos from Photos.app and share them with the 6 | world as animated GIFs. Pretty WIP. 7 | 8 | -------------------------------------------------------------------------------- /LiveGIFs.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | plugin 'cocoapods-keys', { 2 | :project => 'LiveGIFs', 3 | :keys => [ 'ImgurClientId' ] 4 | } 5 | 6 | inhibit_all_warnings! 7 | use_frameworks! 8 | 9 | pod 'FLAnimatedImage' 10 | pod 'ImgurAnonymousAPIClient' 11 | pod 'MRProgress' 12 | -------------------------------------------------------------------------------- /LiveGIFs.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Code/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LiveGIFs 4 | // 5 | // Created by Boris Bügling on 13/10/15. 6 | // Copyright © 2015 Boris Bügling. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 16 | window = UIWindow(frame: UIScreen.mainScreen().bounds) 17 | window?.backgroundColor = UIColor.whiteColor() 18 | window?.rootViewController = UINavigationController(rootViewController: ViewController()) 19 | window?.makeKeyAndVisible() 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Boris Bügling 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Code/ImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCell.swift 3 | // LiveGIFs 4 | // 5 | // Created by Boris Bügling on 13/10/15. 6 | // Copyright © 2015 Boris Bügling. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageCell: UICollectionViewCell { 12 | let imageView: UIImageView 13 | let shadowView: UIView 14 | 15 | override init(frame: CGRect) { 16 | imageView = UIImageView(frame: frame) 17 | imageView.alpha = 0.9 18 | imageView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] 19 | imageView.clipsToBounds = true 20 | imageView.contentMode = .ScaleAspectFill 21 | imageView.layer.cornerRadius = 2.0 22 | 23 | shadowView = UIView(frame: frame) 24 | shadowView.backgroundColor = UIColor.whiteColor() 25 | shadowView.layer.shadowColor = UIColor.blackColor().CGColor 26 | shadowView.layer.shadowOffset = CGSize(width: 2.0, height: 2.0) 27 | shadowView.layer.shadowOpacity = 0.5 28 | 29 | super.init(frame: frame) 30 | 31 | addSubview(shadowView) 32 | addSubview(imageView) 33 | } 34 | 35 | required init(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func layoutSubviews() { 40 | imageView.frame = CGRectInset(bounds, 5.0, 5.0) 41 | shadowView.frame = CGRectInset(bounds, 5.0, 5.0) 42 | 43 | super.layoutSubviews() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | NSAppTransportSecurity 36 | 37 | NSExceptionDomains 38 | 39 | imgur.com 40 | 41 | NSIncludesSubdomains 42 | 43 | NSThirdPartyExceptionRequiresForwardSecrecy 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Resources/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 | -------------------------------------------------------------------------------- /Code/GIFViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GIFViewController.swift 3 | // LiveGIFs 4 | // 5 | // Created by Boris Bügling on 13/10/15. 6 | // Copyright © 2015 Boris Bügling. All rights reserved. 7 | // 8 | 9 | import FLAnimatedImage 10 | import ImgurAnonymousAPIClient 11 | import Keys 12 | import MRProgress 13 | import UIKit 14 | 15 | class GIFViewController: UIViewController { 16 | let fileURL: NSURL 17 | let imageView = FLAnimatedImageView() 18 | 19 | init(fileURL: NSURL) { 20 | self.fileURL = fileURL 21 | super.init(nibName: nil, bundle: nil) 22 | 23 | imageView.animatedImage = FLAnimatedImage(animatedGIFData: NSData(contentsOfURL: fileURL)) 24 | imageView.userInteractionEnabled = true 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | dynamic func shareGIF() { 32 | MRProgressOverlayView.showOverlayAddedTo(self.view, animated: true) 33 | 34 | let client = ImgurAnonymousAPIClient(clientID: LivegifsKeys().imgurClientId()) 35 | client.uploadImageFile(fileURL, withFilename:nil) { (url, error) in 36 | MRProgressOverlayView.dismissOverlayForView(self.view, animated: true) 37 | 38 | if let url = url { 39 | self.navigationController?.presentViewController(UIActivityViewController(activityItems: [url], applicationActivities: nil), animated: true, completion: nil) 40 | } 41 | 42 | if let error = error { 43 | print("Could not upload to imgur: \(error)") 44 | } 45 | } 46 | } 47 | 48 | override func viewDidLoad() { 49 | super.viewDidLoad() 50 | 51 | imageView.frame = view.bounds 52 | view.addSubview(imageView) 53 | 54 | let gestureRecognizer = UITapGestureRecognizer(target: self, action: "shareGIF") 55 | imageView.addGestureRecognizer(gestureRecognizer) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (2.6.1): 3 | - AFNetworking/NSURLConnection (= 2.6.1) 4 | - AFNetworking/NSURLSession (= 2.6.1) 5 | - AFNetworking/Reachability (= 2.6.1) 6 | - AFNetworking/Security (= 2.6.1) 7 | - AFNetworking/Serialization (= 2.6.1) 8 | - AFNetworking/UIKit (= 2.6.1) 9 | - AFNetworking/NSURLConnection (2.6.1): 10 | - AFNetworking/Reachability 11 | - AFNetworking/Security 12 | - AFNetworking/Serialization 13 | - AFNetworking/NSURLSession (2.6.1): 14 | - AFNetworking/Reachability 15 | - AFNetworking/Security 16 | - AFNetworking/Serialization 17 | - AFNetworking/Reachability (2.6.1) 18 | - AFNetworking/Security (2.6.1) 19 | - AFNetworking/Serialization (2.6.1) 20 | - AFNetworking/UIKit (2.6.1): 21 | - AFNetworking/NSURLConnection 22 | - AFNetworking/NSURLSession 23 | - FLAnimatedImage (1.0.8) 24 | - ImgurAnonymousAPIClient (0.3.2): 25 | - AFNetworking (>= 2.3.1) 26 | - Keys (1.0.0) 27 | - MRProgress (0.8.2): 28 | - MRProgress/ActivityIndicator (= 0.8.2) 29 | - MRProgress/Blur (= 0.8.2) 30 | - MRProgress/Circular (= 0.8.2) 31 | - MRProgress/Icons (= 0.8.2) 32 | - MRProgress/NavigationBarProgress (= 0.8.2) 33 | - MRProgress/Overlay (= 0.8.2) 34 | - MRProgress/ActivityIndicator (0.8.2): 35 | - MRProgress/Stopable 36 | - MRProgress/Blur (0.8.2): 37 | - MRProgress/Helper 38 | - MRProgress/Circular (0.8.2): 39 | - MRProgress/Helper 40 | - MRProgress/ProgressBaseClass 41 | - MRProgress/Stopable 42 | - MRProgress/Helper (0.8.2) 43 | - MRProgress/Icons (0.8.2) 44 | - MRProgress/NavigationBarProgress (0.8.2): 45 | - MRProgress/ProgressBaseClass 46 | - MRProgress/Overlay (0.8.2): 47 | - MRProgress/ActivityIndicator 48 | - MRProgress/Blur 49 | - MRProgress/Circular 50 | - MRProgress/Helper 51 | - MRProgress/Icons 52 | - MRProgress/ProgressBaseClass (0.8.2) 53 | - MRProgress/Stopable (0.8.2): 54 | - MRProgress/Helper 55 | 56 | DEPENDENCIES: 57 | - FLAnimatedImage 58 | - ImgurAnonymousAPIClient 59 | - Keys (from `Pods/CocoaPodsKeys`) 60 | - MRProgress 61 | 62 | EXTERNAL SOURCES: 63 | Keys: 64 | :path: Pods/CocoaPodsKeys 65 | 66 | SPEC CHECKSUMS: 67 | AFNetworking: 8e4e60500beb8bec644cf575beee72990a76d399 68 | FLAnimatedImage: f9422f796135aff80d8c00b2afc48015bb746e24 69 | ImgurAnonymousAPIClient: d1887ebe11b64a44ede12928b101d1f1c0099c2b 70 | Keys: 7d2ff6ff42f6903358267ce56e9392f83c31acfe 71 | MRProgress: f4cfe862082418e8f542f077772283f425d6c699 72 | 73 | COCOAPODS: 0.39.0 74 | -------------------------------------------------------------------------------- /Code/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // LiveGIFs 4 | // 5 | // Created by Boris Bügling on 13/10/15. 6 | // Copyright © 2015 Boris Bügling. All rights reserved. 7 | // 8 | 9 | import MRProgress 10 | import Photos 11 | import UIKit 12 | 13 | class ViewController: UICollectionViewController { 14 | static let cellId = NSStringFromClass(ImageCell.self) 15 | 16 | var livePhotos = [BBUAsset]() 17 | 18 | init() { 19 | let size = UIScreen.mainScreen().bounds.size.width / 3.5 20 | let layout = UICollectionViewFlowLayout() 21 | layout.itemSize = CGSize(width: size, height: size) 22 | super.init(collectionViewLayout: layout) 23 | title = "LiveGIFs" 24 | 25 | collectionView?.backgroundColor = UIColor.whiteColor() 26 | collectionView?.registerClass(ImageCell.self, forCellWithReuseIdentifier:ViewController.cellId) 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | let assetCollectionsResult = PHAssetCollection.fetchAssetCollectionsWithType(.SmartAlbum, subtype: .SmartAlbumRecentlyAdded, options: nil) 37 | 38 | if let recentlyAddedCollection = assetCollectionsResult.firstObject as? PHAssetCollection { 39 | let fetchOptions = PHFetchOptions() 40 | fetchOptions.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ] 41 | let recentlyAddedFetchResult = PHAsset.fetchAssetsInAssetCollection(recentlyAddedCollection, options: fetchOptions) 42 | 43 | for i in 0.. UICollectionViewCell { 55 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(ViewController.cellId, forIndexPath: indexPath) 56 | 57 | if cell.tag != 0 { 58 | PHImageManager.defaultManager().cancelImageRequest(PHImageRequestID(cell.tag)) 59 | } 60 | 61 | let livePhoto = livePhotos[indexPath.row] 62 | cell.tag = Int(livePhoto.requestThumbnail(cell.bounds.size) { (result, _) in 63 | (cell as? ImageCell)?.imageView.image = result 64 | }) 65 | 66 | return cell 67 | } 68 | 69 | override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 70 | let livePhoto = livePhotos[indexPath.row] 71 | 72 | MRProgressOverlayView.showOverlayAddedTo(self.view, animated: true) 73 | livePhoto.exportLivePhotoAsGIF() { (fileURL) in 74 | dispatch_sync(dispatch_get_main_queue()) { 75 | MRProgressOverlayView.dismissOverlayForView(self.view, animated: true) 76 | 77 | let viewController = GIFViewController(fileURL: fileURL) 78 | self.navigationController?.pushViewController(viewController, animated: true) 79 | } 80 | } 81 | } 82 | 83 | override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 84 | return livePhotos.count 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Code/GIFication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GIFication.swift 3 | // LiveGIFs 4 | // 5 | // Created by Boris Bügling on 13/10/15. 6 | // Copyright © 2015 Boris Bügling. All rights reserved. 7 | // 8 | 9 | import MobileCoreServices 10 | import Photos 11 | 12 | public struct BBUAsset { 13 | let asset: PHAsset 14 | let manager = PHImageManager.defaultManager() 15 | 16 | public var isLivePhoto: Bool { return resources.count >= 2 } 17 | public var photo: PHAssetResource? { return resource(.Photo) } 18 | public var video: PHAssetResource? { return resource(.PairedVideo) } 19 | 20 | var resources: [PHAssetResource] { return PHAssetResource.assetResourcesForAsset(asset) } 21 | 22 | func resource(type: PHAssetResourceType) -> PHAssetResource? { 23 | for resource in resources { 24 | if resource.type == type { 25 | return resource 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | 32 | public func requestThumbnail(size: CGSize, completionHandler: (UIImage?, [NSObject : AnyObject]?) -> ()) { 33 | manager.requestImageForAsset(asset, targetSize: size, contentMode: .AspectFill, options: nil, resultHandler: completionHandler) 34 | } 35 | 36 | public func exportLivePhotoAsGIF(completionHandler: (fileURL: NSURL) -> ()) { 37 | // PHImageManager.defaultManager().requestAVAssetForVideo(asset, options: nil) {} doesn't work :( 38 | 39 | if let video = video { 40 | let fileName = String(format: "%@_file.mov", NSProcessInfo.processInfo().globallyUniqueString) 41 | let fileURL = NSURL(fileURLWithPath: (NSTemporaryDirectory() as NSString).stringByAppendingPathComponent(fileName)) 42 | defer { let _ = try? NSFileManager.defaultManager().removeItemAtURL(fileURL) } 43 | 44 | PHAssetResourceManager.defaultManager().writeDataForAssetResource(video, toFile: fileURL, options: nil) { (error) in 45 | if let error = error { 46 | NSLog("Could not write file: \(error)") 47 | } else { 48 | let avAsset = AVURLAsset(URL: fileURL) 49 | let duration = Int64(CMTimeGetSeconds(avAsset.duration) + 0.5) 50 | 51 | let track = avAsset.tracksWithMediaType(AVMediaTypeVideo).first! 52 | let frameRate = track.nominalFrameRate 53 | 54 | let imageGenerator = AVAssetImageGenerator(asset: avAsset) 55 | imageGenerator.appliesPreferredTrackTransform = true 56 | imageGenerator.maximumSize = CGSize(width: 720, height: 540) 57 | 58 | let times = self.times(Int32(frameRate), duration) 59 | //times = times + times.reverse() 60 | 61 | let fileProperties = [ String(kCGImagePropertyGIFLoopCount): 0 ] 62 | let frameProperties = [ String(kCGImagePropertyGIFDictionary): [ String(kCGImagePropertyGIFDelayTime): Float(1.0 / frameRate) ] ] 63 | 64 | let documentsDirectoryURL = try? NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) 65 | let fileURL = documentsDirectoryURL!.URLByAppendingPathComponent("animated.gif") 66 | 67 | if let destination = CGImageDestinationCreateWithURL(fileURL, kUTTypeGIF, times.count, nil) { 68 | CGImageDestinationSetProperties(destination, fileProperties) 69 | 70 | var currentImage = 0 71 | imageGenerator.generateCGImagesAsynchronouslyForTimes(times) { (requestedTime, imageRef, actualTime, result, error) in 72 | if let imageRef = imageRef { 73 | CGImageDestinationAddImage(destination, imageRef, frameProperties) 74 | } 75 | 76 | if currentImage == times.count - 1 { 77 | if (!CGImageDestinationFinalize(destination)) { 78 | NSLog("failed to finalize image destination") 79 | } 80 | 81 | completionHandler(fileURL: fileURL) 82 | } 83 | 84 | currentImage++ 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | func times(times: Int32, _ duration: Int64) -> [NSValue] { 93 | var cmtimes = [NSValue]() 94 | 95 | for second in 0..