├── .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 | [](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..