├── .gitignore ├── ExampleApp ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ └── iTunesArtwork@2x.png │ ├── Contents.json │ ├── background-rounded.imageset │ │ ├── Contents.json │ │ └── background-rounded.pdf │ ├── button-camera.imageset │ │ ├── Contents.json │ │ └── button-camera.pdf │ ├── button-photo-library.imageset │ │ ├── Contents.json │ │ └── button-photo-library.pdf │ ├── button_snap.imageset │ │ ├── Contents.json │ │ └── button_snap.pdf │ ├── ic-camera.imageset │ │ ├── Contents.json │ │ └── ic-camera.pdf │ ├── ic-check.imageset │ │ ├── Contents.json │ │ └── ic-check.pdf │ ├── ic-photo.imageset │ │ ├── Contents.json │ │ └── ic-photo.pdf │ ├── icon-depth.imageset │ │ ├── Contents.json │ │ └── icon-depth.pdf │ ├── icon-live.imageset │ │ ├── Contents.json │ │ └── icon-live.pdf │ ├── icon-pano.imageset │ │ ├── Contents.json │ │ └── icon-pano.pdf │ └── icon_flip_camera.imageset │ │ ├── Contents.json │ │ └── icon_flip_camera.pdf ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Custom Views │ ├── CustomImageCell.swift │ ├── CustomImageCell.xib │ ├── CustomVideoCell.swift │ ├── CustomVideoCell.xib │ ├── IconWithTextCell.swift │ └── IconWithTextCell.xib ├── Info.plist ├── ViewController+Helpers.swift └── ViewController.swift ├── ImagePicker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── ImagePicker.xcscheme ├── ImagePicker ├── AVPreviewView.swift ├── ActionCell.swift ├── ActionCell.xib ├── Appearance.swift ├── AssetCell.swift ├── Assets.xcassets │ ├── Contents.json │ ├── background-rounded.imageset │ │ ├── Contents.json │ │ └── background-rounded.pdf │ ├── button-camera.imageset │ │ ├── Contents.json │ │ └── button-camera.pdf │ ├── button-photo-library.imageset │ │ ├── Contents.json │ │ └── button-photo-library.pdf │ ├── gradient.imageset │ │ ├── Contents.json │ │ ├── gradient.png │ │ ├── gradient@2x.png │ │ └── gradient@3x.png │ ├── icon-badge-livephoto.imageset │ │ ├── Contents.json │ │ └── icon-badge-livephoto.pdf │ ├── icon-badge-video.imageset │ │ ├── Contents.json │ │ └── icon-badge-video.pdf │ ├── icon-check-background.imageset │ │ ├── Contents.json │ │ └── icon-ckeck-background.pdf │ ├── icon-check.imageset │ │ ├── Contents.json │ │ └── icon-check.pdf │ ├── icon-flip-camera.imageset │ │ ├── Contents.json │ │ └── flipCamera.pdf │ ├── icon-live-off.imageset │ │ ├── Contents.json │ │ └── icon-live-off.pdf │ └── icon-live-on.imageset │ │ ├── Contents.json │ │ └── icon-live-on.pdf ├── AsynchronousOperation.swift ├── CameraCollectionViewCell.swift ├── CaptureSession.swift ├── CaptureSettings.swift ├── CarvedLabel.swift ├── CellRegistrator.swift ├── CollectionViewBatchAnimation.swift ├── CollectionViewUpdatesCoordinator.swift ├── ImagePicker.h ├── ImagePickerAssetModel.swift ├── ImagePickerController.swift ├── ImagePickerDataSource.swift ├── ImagePickerDelegate.swift ├── ImagePickerLayout.swift ├── ImagePickerSelectionPolicy.swift ├── ImagePickerView.swift ├── ImagePickerView.xib ├── Info.plist ├── LayoutConfiguration.swift ├── LayoutModel.swift ├── LivePhotoCameraCell.swift ├── LivePhotoCameraCell.xib ├── Miscellaneous.swift ├── PhotoCaptureDelegate.swift ├── RecordButton.swift ├── RecordDurationLabel.swift ├── ShutterButton.swift ├── StationaryButton.swift ├── UIImageEffects.h ├── UIImageEffects.m ├── VideoCameraCell.swift ├── VideoCameraCell.xib ├── VideoCaptureDelegate.swift └── VideoOuptutSampleBufferDelegate.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/.gitignore -------------------------------------------------------------------------------- /ExampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 04/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // 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. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // 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. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // 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. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // 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. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-60x60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "1024x1024", 53 | "idiom" : "ios-marketing", 54 | "filename" : "iTunesArtwork@2x.png", 55 | "scale" : "1x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/background-rounded.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "background-rounded.pdf", 6 | "resizing" : { 7 | "mode" : "9-part", 8 | "center" : { 9 | "mode" : "tile", 10 | "width" : 1, 11 | "height" : 1 12 | }, 13 | "cap-insets" : { 14 | "bottom" : 12, 15 | "top" : 12, 16 | "right" : 12, 17 | "left" : 12 18 | } 19 | } 20 | } 21 | ], 22 | "info" : { 23 | "version" : 1, 24 | "author" : "xcode" 25 | }, 26 | "properties" : { 27 | "template-rendering-intent" : "template" 28 | } 29 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/background-rounded.imageset/background-rounded.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/background-rounded.imageset/background-rounded.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/button-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-camera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/button-camera.imageset/button-camera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/button-camera.imageset/button-camera.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/button-photo-library.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-photo-library.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/button_snap.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button_snap.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/button_snap.imageset/button_snap.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/button_snap.imageset/button_snap.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/ic-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic-camera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/ic-camera.imageset/ic-camera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/ic-camera.imageset/ic-camera.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/ic-check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic-check.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/ic-check.imageset/ic-check.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/ic-check.imageset/ic-check.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/ic-photo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic-photo.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/ic-photo.imageset/ic-photo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/ic-photo.imageset/ic-photo.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon-depth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-depth.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon-depth.imageset/icon-depth.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/icon-depth.imageset/icon-depth.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon-live.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon-live.imageset/icon-live.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/icon-live.imageset/icon-live.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon-pano.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-pano.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon-pano.imageset/icon-pano.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/icon-pano.imageset/icon-pano.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon_flip_camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_flip_camera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/icon_flip_camera.imageset/icon_flip_camera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ExampleApp/Assets.xcassets/icon_flip_camera.imageset/icon_flip_camera.pdf -------------------------------------------------------------------------------- /ExampleApp/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 | -------------------------------------------------------------------------------- /ExampleApp/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 | -------------------------------------------------------------------------------- /ExampleApp/Custom Views/CustomImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCell.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 07/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import ImagePicker 12 | 13 | class CustomImageCell : UICollectionViewCell, ImagePickerAssetCell { 14 | 15 | @IBOutlet weak var imageView: UIImageView! 16 | 17 | var representedAssetIdentifier: String? 18 | 19 | @IBOutlet weak var subtypeImageView: UIImageView! 20 | @IBOutlet weak var selectedImageView: UIImageView! 21 | 22 | override var isSelected: Bool { 23 | didSet { 24 | selectedImageView.isHidden = !isSelected 25 | } 26 | } 27 | 28 | override func awakeFromNib() { 29 | super.awakeFromNib() 30 | subtypeImageView.backgroundColor = UIColor.clear 31 | selectedImageView.isHidden = !isSelected 32 | } 33 | 34 | override func prepareForReuse() { 35 | super.prepareForReuse() 36 | imageView.image = nil 37 | subtypeImageView.image = nil 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ExampleApp/Custom Views/CustomImageCell.xib: -------------------------------------------------------------------------------- 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 | 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 | -------------------------------------------------------------------------------- /ExampleApp/Custom Views/CustomVideoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCell.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 11/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ImagePicker 11 | 12 | class CustomVideoCell: UICollectionViewCell, ImagePickerAssetCell { 13 | 14 | @IBOutlet weak var imageView: UIImageView! 15 | 16 | var representedAssetIdentifier: String? 17 | 18 | @IBOutlet weak var label: UILabel! 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | // Initialization code 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ExampleApp/Custom Views/CustomVideoCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ExampleApp/Custom Views/IconWithTextCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconWithTextCell.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 06/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class IconWithTextCell : UICollectionViewCell { 13 | 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var imageView: UIImageView! 16 | @IBOutlet weak var topOffset: NSLayoutConstraint! 17 | @IBOutlet weak var bottomOffset: NSLayoutConstraint! 18 | 19 | private var originalBackgroundColor: UIColor? 20 | 21 | override var isHighlighted: Bool { 22 | didSet { 23 | if isHighlighted { 24 | backgroundColor = UIColor.red 25 | } 26 | else { 27 | backgroundColor = originalBackgroundColor 28 | } 29 | } 30 | } 31 | 32 | override func awakeFromNib() { 33 | super.awakeFromNib() 34 | originalBackgroundColor = backgroundColor 35 | imageView.backgroundColor = UIColor.clear 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ExampleApp/Custom Views/IconWithTextCell.xib: -------------------------------------------------------------------------------- 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 | 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 | -------------------------------------------------------------------------------- /ExampleApp/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 | 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 | NSPhotoLibraryUsageDescription 38 | App uses access to Photos when picking images 39 | NSCameraUsageDescription 40 | Camera is used by Image picker when taking new photos or recording videos 41 | NSMicrophoneUsageDescription 42 | Microphone is used by Image picker when recording videos 43 | 44 | 45 | -------------------------------------------------------------------------------- /ExampleApp/ViewController+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseTableViewController.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 03/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: UIResponsder Methods 12 | 13 | extension ViewController { 14 | 15 | override var canBecomeFirstResponder: Bool { 16 | return true 17 | } 18 | 19 | override func resignFirstResponder() -> Bool { 20 | let result = super.resignFirstResponder() 21 | if result == true { 22 | currentInputView = nil 23 | } 24 | return result 25 | } 26 | 27 | override var inputView: UIView? { 28 | return currentInputView 29 | } 30 | 31 | override var inputAccessoryView: UIView? { 32 | return presentButton 33 | } 34 | 35 | } 36 | 37 | // MARK: UITableViewDataSource & UITableViewDelegate Methods 38 | 39 | extension ViewController { 40 | 41 | override func numberOfSections(in tableView: UITableView) -> Int { 42 | return cellsData.count 43 | } 44 | 45 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 | return cellsData[section].count 47 | } 48 | 49 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 50 | let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) 51 | cell.textLabel?.text = cellsData[indexPath.section][indexPath.row].title 52 | if let configBlock = cellsData[indexPath.section][indexPath.row].configBlock { 53 | configBlock(cell, self) 54 | } 55 | return cell 56 | } 57 | 58 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 59 | 60 | // deselect 61 | tableView.deselectRow(at: indexPath, animated: true) 62 | 63 | // perform selector 64 | let selector = cellsData[indexPath.section][indexPath.row].selector 65 | let argumentType = cellsData[indexPath.section][indexPath.row].selectorArgument 66 | switch argumentType { 67 | case .indexPath: perform(selector, with: indexPath) 68 | default: perform(selector) 69 | } 70 | 71 | // update checks in section 72 | uncheckCellsInSection(except: indexPath) 73 | } 74 | 75 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 76 | return sectionsData[section].0 77 | } 78 | 79 | override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 80 | return sectionsData[section].1 81 | } 82 | 83 | } 84 | 85 | // MARK: Helper Code 86 | 87 | enum SelectorArgument { 88 | case indexPath 89 | case none 90 | } 91 | 92 | struct CellData { 93 | var title: String 94 | var selector: Selector 95 | var selectorArgument: SelectorArgument 96 | var configBlock: CellConfigurationBlock 97 | 98 | init(_ title: String, _ selector: Selector, _ selectorArgument: SelectorArgument, _ configBlock: CellConfigurationBlock) { 99 | self.title = title 100 | self.selector = selector 101 | self.selectorArgument = selectorArgument 102 | self.configBlock = configBlock 103 | } 104 | } 105 | 106 | typealias CellConfigurationBlock = ((UITableViewCell, ViewController) -> Void)? 107 | 108 | extension ViewController { 109 | 110 | static let durationFormatter: DateComponentsFormatter = { 111 | let formatter = DateComponentsFormatter() 112 | formatter.unitsStyle = .positional 113 | formatter.allowedUnits = [.minute, .second] 114 | formatter.zeroFormattingBehavior = .pad 115 | return formatter 116 | }() 117 | 118 | func uncheckCellsInSection(except indexPath: IndexPath){ 119 | for path in tableView.indexPathsForVisibleRows ?? [] where path.section == indexPath.section { 120 | let cell = tableView.cellForRow(at: path)! 121 | cell.accessoryType = path == indexPath ? .checkmark : .none 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /ImagePicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImagePicker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ImagePicker.xcodeproj/xcshareddata/xcschemes/ImagePicker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ImagePicker/AVPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPreviewView.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 27/03/17. 6 | // Copyright © 2017 Peter Stajger. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import AVFoundation 12 | 13 | enum VideoDisplayMode { 14 | /// Preserve aspect ratio, fit within layer bounds. 15 | case aspectFit 16 | /// Preserve aspect ratio, fill view bounds. 17 | case aspectFill 18 | ///Stretch to fill layer bounds 19 | case resize 20 | } 21 | 22 | /// 23 | /// A view whose layer is AVCaptureVideoPreviewLayer so it's used for previewing 24 | /// output from a capture session. 25 | /// 26 | final class AVPreviewView: UIView { 27 | 28 | deinit { 29 | log("deinit: \(String(describing: self))") 30 | } 31 | 32 | var previewLayer: AVCaptureVideoPreviewLayer { 33 | return layer as! AVCaptureVideoPreviewLayer 34 | } 35 | 36 | var session: AVCaptureSession? { 37 | get { return previewLayer.session } 38 | set { 39 | if previewLayer.session === newValue { 40 | return 41 | } 42 | previewLayer.session = newValue 43 | 44 | } 45 | } 46 | 47 | var displayMode: VideoDisplayMode = .aspectFill { 48 | didSet { applyVideoDisplayMode() } 49 | } 50 | 51 | override class var layerClass: AnyClass { 52 | return AVCaptureVideoPreviewLayer.self 53 | } 54 | 55 | override init(frame: CGRect) { 56 | super.init(frame: frame) 57 | applyVideoDisplayMode() 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | super.init(coder: aDecoder) 62 | applyVideoDisplayMode() 63 | } 64 | 65 | // MARK: Private Methods 66 | 67 | private func applyVideoDisplayMode() { 68 | switch displayMode { 69 | case .aspectFill: previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill 70 | case .aspectFit: previewLayer.videoGravity = AVLayerVideoGravity.resizeAspect 71 | case .resize: previewLayer.videoGravity = AVLayerVideoGravity.resize 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ImagePicker/ActionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconWithTextCell.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 06/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | final class ActionCell : UICollectionViewCell { 13 | 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var imageView: UIImageView! 16 | 17 | @IBOutlet var leadingOffset: NSLayoutConstraint! 18 | @IBOutlet var trailingOffset: NSLayoutConstraint! 19 | @IBOutlet var topOffset: NSLayoutConstraint! 20 | @IBOutlet var bottomOffset: NSLayoutConstraint! 21 | 22 | override func awakeFromNib() { 23 | super.awakeFromNib() 24 | imageView.backgroundColor = UIColor.clear 25 | } 26 | 27 | } 28 | 29 | extension ActionCell { 30 | 31 | func update(withIndex index: Int, layoutConfiguration: LayoutConfiguration) { 32 | 33 | let layoutModel = LayoutModel(configuration: layoutConfiguration, assets: 0) 34 | let actionCount = layoutModel.numberOfItems(in: layoutConfiguration.sectionIndexForActions) 35 | 36 | titleLabel.textColor = UIColor.black 37 | switch index { 38 | case 0: 39 | titleLabel.text = "Camera" 40 | imageView.image = UIImage(named: "button-camera", in: Bundle(for: type(of: self)), compatibleWith: nil) 41 | case 1: 42 | titleLabel.text = "Photos" 43 | imageView.image = UIImage(named: "button-photo-library", in: Bundle(for: type(of: self)), compatibleWith: nil) 44 | default: break 45 | } 46 | 47 | let isFirst = index == 0 48 | let isLast = index == actionCount - 1 49 | 50 | switch layoutConfiguration.scrollDirection { 51 | case .horizontal: 52 | topOffset.constant = isFirst ? 10 : 5 53 | bottomOffset.constant = isLast ? 10 : 5 54 | leadingOffset.constant = 5 55 | trailingOffset.constant = 5 56 | case .vertical: 57 | topOffset.constant = 5 58 | bottomOffset.constant = 5 59 | leadingOffset.constant = isFirst ? 10 : 5 60 | trailingOffset.constant = isLast ? 10 : 5 61 | } 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ImagePicker/ActionCell.xib: -------------------------------------------------------------------------------- 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 | 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 | 82 | -------------------------------------------------------------------------------- /ImagePicker/Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Appearance.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 26/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// Provides access to styling attributes of Image Picker. 13 | /// 14 | public class Appearance { 15 | /// 16 | /// Image picker background color. 17 | /// 18 | public var backgroundColor: UIColor = UIColor(red: 208/255, green: 213/255, blue: 218/255, alpha: 1) 19 | } 20 | -------------------------------------------------------------------------------- /ImagePicker/AssetCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetCell.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 27/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | /// 13 | /// Each image picker asset cell must conform to this protocol. 14 | /// 15 | public protocol ImagePickerAssetCell : class { 16 | 17 | /// This image view will be used when setting an asset's image 18 | var imageView: UIImageView! { get } 19 | 20 | /// This is a helper identifier that is used when properly displaying cells asynchronously 21 | var representedAssetIdentifier: String? { get set } 22 | } 23 | 24 | /// 25 | /// A default collection view cell that represents asset item. It supports: 26 | /// - shows image view of image thumbnail 27 | /// - icon and duration for videos 28 | /// - selected icon when isSelected is true 29 | /// 30 | class VideoAssetCell : AssetCell { 31 | 32 | var durationLabel: UILabel 33 | var iconView: UIImageView 34 | var gradientView: UIImageView 35 | 36 | override init(frame: CGRect) { 37 | 38 | durationLabel = UILabel(frame: .zero) 39 | gradientView = UIImageView(frame: .zero) 40 | iconView = UIImageView(frame: .zero) 41 | 42 | super.init(frame: frame) 43 | 44 | gradientView.isHidden = true 45 | 46 | iconView.tintColor = UIColor.white 47 | iconView.contentMode = .center 48 | 49 | durationLabel.textColor = UIColor.white 50 | durationLabel.font = UIFont.systemFont(ofSize: 12, weight: .semibold) 51 | durationLabel.textAlignment = .right 52 | 53 | contentView.addSubview(gradientView) 54 | contentView.addSubview(durationLabel) 55 | contentView.addSubview(iconView) 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | fatalError("init(coder:) has not been implemented") 60 | } 61 | 62 | override func layoutSubviews() { 63 | super.layoutSubviews() 64 | 65 | gradientView.frame.size = CGSize(width: bounds.width, height: 40) 66 | gradientView.frame.origin = CGPoint(x: 0, y: bounds.height-40) 67 | 68 | let margin: CGFloat = 5 69 | durationLabel.frame.size = CGSize(width: 50, height: 20) 70 | durationLabel.frame.origin = CGPoint( 71 | x: contentView.bounds.width - durationLabel.frame.size.width - margin, 72 | y: contentView.bounds.height - durationLabel.frame.size.height - margin 73 | ) 74 | iconView.frame.size = CGSize(width: 21, height: 21) 75 | iconView.frame.origin = CGPoint( 76 | x: margin, 77 | y: contentView.bounds.height - iconView.frame.height - margin 78 | ) 79 | } 80 | 81 | static let durationFormatter: DateComponentsFormatter = { 82 | let formatter = DateComponentsFormatter() 83 | formatter.unitsStyle = .positional 84 | formatter.allowedUnits = [.minute, .second] 85 | formatter.zeroFormattingBehavior = .pad 86 | return formatter 87 | }() 88 | 89 | func update(with asset: PHAsset) { 90 | 91 | switch asset.mediaType { 92 | case .image: 93 | if asset.mediaSubtypes.contains(.photoLive) { 94 | gradientView.isHidden = false 95 | gradientView.image = UIImage(named: "gradient", in: Bundle(for: type(of: self)), compatibleWith: nil)?.resizableImage(withCapInsets: .zero, resizingMode: .stretch) 96 | iconView.isHidden = false 97 | durationLabel.isHidden = true 98 | iconView.image = UIImage(named: "icon-badge-livephoto", in: Bundle(for: type(of: self)), compatibleWith: nil) 99 | } 100 | else { 101 | gradientView.isHidden = true 102 | iconView.isHidden = true 103 | durationLabel.isHidden = true 104 | } 105 | case .video: 106 | gradientView.isHidden = false 107 | gradientView.image = UIImage(named: "gradient", in: Bundle(for: type(of: self)), compatibleWith: nil)?.resizableImage(withCapInsets: .zero, resizingMode: .stretch) 108 | iconView.isHidden = false 109 | durationLabel.isHidden = false 110 | iconView.image = UIImage(named: "icon-badge-video", in: Bundle(for: type(of: self)), compatibleWith: nil) 111 | durationLabel.text = VideoAssetCell.durationFormatter.string(from: asset.duration) 112 | default: break 113 | } 114 | 115 | } 116 | 117 | } 118 | 119 | /// 120 | /// A default implementation of `ImagePickerAssetCell`. If user does not register 121 | /// a custom cell, Image Picker will use this one. Also contains 122 | /// default icon for selected state. 123 | /// 124 | class AssetCell : UICollectionViewCell, ImagePickerAssetCell { 125 | 126 | var imageView: UIImageView! = UIImageView(frame: .zero) 127 | fileprivate var selectedImageView = CheckView(frame: .zero) 128 | 129 | var representedAssetIdentifier: String? 130 | 131 | override var isSelected: Bool { 132 | didSet { 133 | selectedImageView.isHidden = !isSelected 134 | if selectedImageView.isHidden == false { 135 | selectedImageView.image = UIImage(named: "icon-check-background", in: Bundle(for: type(of: self)), compatibleWith: nil) 136 | selectedImageView.foregroundImage = UIImage(named: "icon-check", in: Bundle(for: type(of: self)), compatibleWith: nil) 137 | } 138 | } 139 | } 140 | 141 | override init(frame: CGRect) { 142 | super.init(frame: frame) 143 | imageView.contentMode = .scaleAspectFill 144 | imageView.clipsToBounds = true 145 | contentView.addSubview(imageView) 146 | 147 | selectedImageView.frame = CGRect(x: 0, y: 0, width: 31, height: 31) 148 | 149 | contentView.addSubview(selectedImageView) 150 | selectedImageView.isHidden = true 151 | } 152 | 153 | required init?(coder aDecoder: NSCoder) { 154 | fatalError("init(coder:) has not been implemented") 155 | } 156 | 157 | override func prepareForReuse() { 158 | super.prepareForReuse() 159 | imageView.image = nil 160 | } 161 | 162 | override func layoutSubviews() { 163 | super.layoutSubviews() 164 | imageView.frame = bounds 165 | let margin: CGFloat = 5 166 | selectedImageView.frame.origin = CGPoint( 167 | x: bounds.width - selectedImageView.frame.width - margin, 168 | y: margin 169 | ) 170 | } 171 | 172 | } 173 | 174 | private final class CheckView : UIImageView { 175 | 176 | var foregroundImage: UIImage? { 177 | get { return foregroundView.image } 178 | set { foregroundView.image = newValue } 179 | } 180 | 181 | private let foregroundView = UIImageView(frame: .zero) 182 | 183 | override init(frame: CGRect) { 184 | super.init(frame: frame) 185 | addSubview(foregroundView) 186 | contentMode = .center 187 | foregroundView.contentMode = .center 188 | } 189 | 190 | required init?(coder aDecoder: NSCoder) { 191 | fatalError("init(coder:) has not been implemented") 192 | } 193 | 194 | override func layoutSubviews() { 195 | super.layoutSubviews() 196 | foregroundView.frame = bounds 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/background-rounded.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "background-rounded.pdf", 6 | "resizing" : { 7 | "mode" : "9-part", 8 | "center" : { 9 | "mode" : "tile", 10 | "width" : 1, 11 | "height" : 1 12 | }, 13 | "cap-insets" : { 14 | "bottom" : 12, 15 | "top" : 12, 16 | "right" : 12, 17 | "left" : 12 18 | } 19 | } 20 | } 21 | ], 22 | "info" : { 23 | "version" : 1, 24 | "author" : "xcode" 25 | }, 26 | "properties" : { 27 | "template-rendering-intent" : "template" 28 | } 29 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/background-rounded.imageset/background-rounded.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/background-rounded.imageset/background-rounded.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/button-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-camera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/button-camera.imageset/button-camera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/button-camera.imageset/button-camera.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/button-photo-library.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "button-photo-library.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/button-photo-library.imageset/button-photo-library.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/gradient.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "gradient.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "gradient@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "gradient@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/gradient.imageset/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/gradient.imageset/gradient.png -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/gradient.imageset/gradient@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/gradient.imageset/gradient@2x.png -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/gradient.imageset/gradient@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/gradient.imageset/gradient@3x.png -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-badge-livephoto.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-badge-livephoto.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-badge-livephoto.imageset/icon-badge-livephoto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-badge-livephoto.imageset/icon-badge-livephoto.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-badge-video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-badge-video.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-badge-video.imageset/icon-badge-video.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-badge-video.imageset/icon-badge-video.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-check-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-ckeck-background.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-check-background.imageset/icon-ckeck-background.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-check-background.imageset/icon-ckeck-background.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-check.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-check.imageset/icon-check.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-check.imageset/icon-check.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-flip-camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "flipCamera.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-flip-camera.imageset/flipCamera.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-flip-camera.imageset/flipCamera.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-live-off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live-off.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-live-off.imageset/icon-live-off.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-live-off.imageset/icon-live-off.pdf -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-live-on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon-live-on.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /ImagePicker/Assets.xcassets/icon-live-on.imageset/icon-live-on.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inloop/image-picker/ccfbbffc1b245f5088c2b899ea4c41a5b36f5c36/ImagePicker/Assets.xcassets/icon-live-on.imageset/icon-live-on.pdf -------------------------------------------------------------------------------- /ImagePicker/AsynchronousOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsynchronousOperation.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 13/04/2018. 6 | // Copyright © 2018 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// Provides primitives for wrapping Operation with asynchronous code that can 13 | /// be enqueued in a OperationQueue and run serially. 14 | /// 15 | class AsynchronousOperation : Foundation.Operation { 16 | 17 | var stateFinished: Bool = false { 18 | willSet { willChangeValue(forKey: "isFinished") } 19 | didSet { didChangeValue(forKey: "isFinished") } 20 | } 21 | var stateExecuting: Bool = false { 22 | willSet { willChangeValue(forKey: "isExecuting") } 23 | didSet { didChangeValue(forKey: "isExecuting") } 24 | } 25 | 26 | override var isFinished: Bool { 27 | return stateFinished 28 | } 29 | 30 | override var isExecuting: Bool { 31 | return stateExecuting 32 | } 33 | 34 | override var isAsynchronous: Bool { 35 | return true 36 | } 37 | 38 | override func main() { 39 | if isCancelled { 40 | completeOperation() 41 | } 42 | else { 43 | stateExecuting = true 44 | execute() 45 | } 46 | } 47 | 48 | func execute() { 49 | fatalError("This method has to be overriden") 50 | } 51 | 52 | func completeOperation() { 53 | if self.stateExecuting == true { 54 | self.stateExecuting = false 55 | } 56 | 57 | if self.stateFinished == false { 58 | self.stateFinished = true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ImagePicker/CameraCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraCollectionViewCell.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 08/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import AVFoundation 12 | 13 | protocol CameraCollectionViewCellDelegate : class { 14 | func takePicture() 15 | func takeLivePhoto() 16 | func startVideoRecording() 17 | func stopVideoRecording() 18 | func flipCamera(_ completion: (() -> Void)?) 19 | } 20 | 21 | /// 22 | /// Each custom camera cell must inherit from this base class. 23 | /// 24 | open class CameraCollectionViewCell : UICollectionViewCell { 25 | 26 | deinit { 27 | log("deinit: \(String(describing: self))") 28 | } 29 | 30 | /// contains video preview layer 31 | var previewView: AVPreviewView = { 32 | let view = AVPreviewView(frame: .zero) 33 | view.backgroundColor = UIColor.black 34 | return view 35 | }() 36 | 37 | /// 38 | /// holds static image that is above blur view to achieve nicer presentation 39 | /// - note: when capture session is interrupted, there is no input stream so 40 | /// output is black, adding image here will nicely hide this black background 41 | /// 42 | var imageView: UIImageView = { 43 | let view = UIImageView(frame: .zero) 44 | view.contentMode = .scaleAspectFill 45 | return view 46 | }() 47 | 48 | var blurView: UIVisualEffectView? 49 | 50 | var isVisualEffectViewUsedForBlurring = false 51 | 52 | weak var delegate: CameraCollectionViewCellDelegate? 53 | 54 | // MARK: View Lifecycle Methods 55 | 56 | public override init(frame: CGRect) { 57 | super.init(frame: frame) 58 | backgroundView = previewView 59 | previewView.addSubview(imageView) 60 | } 61 | 62 | required public init?(coder aDecoder: NSCoder) { 63 | super.init(coder: aDecoder) 64 | backgroundView = previewView 65 | previewView.addSubview(imageView) 66 | } 67 | 68 | open override func layoutSubviews() { 69 | super.layoutSubviews() 70 | imageView.frame = previewView.bounds 71 | blurView?.frame = previewView.bounds 72 | } 73 | 74 | // MARK: Public Methods 75 | 76 | /// 77 | /// The cell can have multiple visual states based on autorization status. Use 78 | /// `updateCameraAuthorizationStatus()` func to udate UI. 79 | /// 80 | public internal(set) var authorizationStatus: AVAuthorizationStatus? { 81 | didSet { updateCameraAuthorizationStatus() } 82 | } 83 | 84 | /// 85 | /// Called each time an authorization status to camera is changed. Update your 86 | /// cell's UI based on current value of `authorizationStatus` property. 87 | /// 88 | open func updateCameraAuthorizationStatus() { 89 | 90 | } 91 | 92 | /// 93 | /// If live photos are enabled this method is called each time user captures 94 | /// a live photo. Override this method to update UI based on live view status. 95 | /// 96 | /// - parameter isProcessing: If there is at least 1 live photo being processed/captured 97 | /// - parameter shouldAnimate: If the UI change should be animated or not. 98 | /// 99 | open func updateLivePhotoStatus(isProcessing: Bool, shouldAnimate: Bool) { 100 | 101 | } 102 | 103 | /// 104 | /// If video recording is enabled this method is called each time user starts or stops 105 | /// a recording. Override this method to update UI based on recording status. 106 | /// 107 | /// - parameter isRecording: If video is recording or not 108 | /// - parameter shouldAnimate: If the UI change should be animated or not. 109 | /// 110 | open func updateRecordingVideoStatus(isRecording: Bool, shouldAnimate: Bool) { 111 | 112 | } 113 | 114 | open func videoRecodingDidBecomeReady() { 115 | 116 | } 117 | 118 | /// 119 | /// Flips camera from front/rear or rear/front. Flip is always supplemented with 120 | /// an flip animation. 121 | /// 122 | /// - parameter completion: A block is called as soon as camera is changed. 123 | /// 124 | @objc public func flipCamera(_ completion: (() -> Void)? = nil) { 125 | delegate?.flipCamera(completion) 126 | } 127 | 128 | /// 129 | /// Takes a picture 130 | /// 131 | @objc public func takePicture() { 132 | delegate?.takePicture() 133 | } 134 | 135 | /// 136 | /// Takes a live photo. Please note that live photos must be enabled when configuring Image Picker. 137 | /// 138 | @objc public func takeLivePhoto() { 139 | delegate?.takeLivePhoto() 140 | } 141 | 142 | @objc public func startVideoRecording() { 143 | delegate?.startVideoRecording() 144 | } 145 | 146 | @objc public func stopVideoRecording() { 147 | delegate?.stopVideoRecording() 148 | } 149 | 150 | // MARK: Internal Methods 151 | 152 | func blurIfNeeded(blurImage: UIImage?, animated: Bool, completion: ((Bool) -> Void)?) { 153 | 154 | var view: UIView 155 | 156 | if isVisualEffectViewUsedForBlurring == false { 157 | 158 | guard imageView.image == nil else { 159 | return 160 | } 161 | 162 | imageView.image = blurImage 163 | 164 | view = imageView 165 | } 166 | else { 167 | 168 | if blurView == nil { 169 | blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) 170 | previewView.addSubview(blurView!) 171 | } 172 | 173 | view = blurView! 174 | view.frame = previewView.bounds 175 | } 176 | 177 | view.alpha = 0 178 | 179 | if animated == false { 180 | view.alpha = 1 181 | completion?(true) 182 | } 183 | else { 184 | UIView.animate(withDuration: 0.1, delay: 0, options: .allowAnimatedContent, animations: { 185 | view.alpha = 1 186 | }, completion: completion) 187 | } 188 | } 189 | 190 | func unblurIfNeeded(unblurImage: UIImage?, animated: Bool, completion: ((Bool) -> Void)?) { 191 | 192 | var animationBlock: () -> () 193 | var animationCompletionBlock: (Bool) -> () 194 | 195 | if isVisualEffectViewUsedForBlurring == false { 196 | 197 | guard imageView.image != nil else { 198 | return 199 | } 200 | 201 | if let image = unblurImage { 202 | imageView.image = image 203 | } 204 | 205 | animationBlock = { 206 | self.imageView.alpha = 0 207 | } 208 | 209 | animationCompletionBlock = { finished in 210 | self.imageView.image = nil 211 | completion?(finished) 212 | } 213 | } 214 | else { 215 | 216 | animationBlock = { 217 | self.blurView?.alpha = 0 218 | } 219 | 220 | animationCompletionBlock = { finished in 221 | completion?(finished) 222 | } 223 | } 224 | 225 | if animated == false { 226 | animationBlock() 227 | animationCompletionBlock(true) 228 | } 229 | else { 230 | UIView.animate(withDuration: 0.1, delay: 0, options: .allowAnimatedContent, animations: animationBlock, completion: animationCompletionBlock) 231 | } 232 | } 233 | 234 | /// 235 | /// When user taps a camera cell this method is called and the result is 236 | /// used when determining whether the tap should take a photo or not. This 237 | /// is used when user taps on a button so the button is triggered not the touch. 238 | /// 239 | func touchIsCaptureEffective(point: CGPoint) -> Bool { 240 | // find the topmost view that detected the touch at point and check if it's not any button or anything other than contentView 241 | if bounds.contains(point), let testedView = hitTest(point, with: nil), testedView === contentView { 242 | return true 243 | } 244 | return false 245 | } 246 | 247 | 248 | } 249 | -------------------------------------------------------------------------------- /ImagePicker/CaptureSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureSettings.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 25/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// Configure capture session using this struct. 13 | /// 14 | public struct CaptureSettings { 15 | 16 | public enum CameraMode { 17 | /// 18 | /// If you support only photos use this preset. Default value. 19 | /// 20 | case photo 21 | /// 22 | /// If you know you will use live photos use this preset. 23 | /// 24 | case photoAndLivePhoto 25 | 26 | /// If you wish to record videos or take photos. 27 | case photoAndVideo 28 | } 29 | 30 | /// 31 | /// Capture session uses this preset when configuring. Select a preset of 32 | /// media types you wish to support. 33 | /// 34 | /// - note: currently you can not change preset at runtime 35 | /// 36 | public var cameraMode: CameraMode 37 | 38 | /// 39 | /// Return true if captured photos will be saved to photo library. Image picker 40 | /// will prompt user with request for permisssions when needed. Default value is false 41 | /// for photos. Live photos and videos are always true. 42 | /// 43 | /// - note: please note, that at current implementation this applies to photos only. For 44 | /// live photos and videos this is always true. 45 | /// 46 | public var savesCapturedPhotosToPhotoLibrary: Bool 47 | 48 | let savesCapturedLivePhotosToPhotoLibrary: Bool = true 49 | let savesCapturedVideosToPhotoLibrary: Bool = true 50 | 51 | /// Default configuration 52 | public static var `default`: CaptureSettings { 53 | return CaptureSettings( 54 | cameraMode: .photo, 55 | savesCapturedPhotosToPhotoLibrary: false 56 | ) 57 | } 58 | } 59 | 60 | extension CaptureSettings.CameraMode { 61 | 62 | /// transforms user related enum to specific internal capture session enum 63 | var captureSessionPresetConfiguration: CaptureSession.SessionPresetConfiguration { 64 | switch self { 65 | case .photo: return .photos 66 | case .photoAndLivePhoto: return .livePhotos 67 | case .photoAndVideo: return .videos 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /ImagePicker/CarvedLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarvedLabel.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 26/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | fileprivate typealias TextAttributes = [NSAttributedStringKey: Any] 12 | 13 | /// 14 | /// A label whose transparent text is carved into solid color. 15 | /// 16 | /// - please note that text is always aligned to center 17 | /// 18 | @IBDesignable 19 | final class CarvedLabel : UIView { 20 | 21 | @IBInspectable var text: String? { 22 | didSet { 23 | invalidateIntrinsicContentSize() 24 | setNeedsDisplay() 25 | } 26 | } 27 | 28 | var font: UIFont? { 29 | didSet { 30 | invalidateIntrinsicContentSize() 31 | setNeedsDisplay() 32 | } 33 | } 34 | 35 | @IBInspectable var cornerRadius: CGFloat = 0 { 36 | didSet { setNeedsDisplay() } 37 | } 38 | 39 | @IBInspectable var verticalInset: CGFloat = 0 { 40 | didSet { 41 | invalidateIntrinsicContentSize() 42 | setNeedsDisplay() 43 | } 44 | } 45 | 46 | @IBInspectable var horizontalInset: CGFloat = 0 { 47 | didSet { 48 | invalidateIntrinsicContentSize() 49 | setNeedsDisplay() 50 | } 51 | 52 | } 53 | 54 | override init(frame: CGRect) { 55 | super.init(frame: frame) 56 | _ = backgroundColor 57 | isOpaque = false 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | super.init(coder: aDecoder) 62 | _ = backgroundColor 63 | isOpaque = false 64 | } 65 | 66 | override var backgroundColor: UIColor? { 67 | get { return UIColor.clear } 68 | set { super.backgroundColor = UIColor.clear } 69 | } 70 | 71 | fileprivate var textAttributes: TextAttributes { 72 | let activeFont = font ?? UIFont.systemFont(ofSize: 12, weight: .regular) 73 | return [ 74 | NSAttributedStringKey.font: activeFont 75 | ] 76 | } 77 | 78 | fileprivate var attributedString: NSAttributedString { 79 | return NSAttributedString(string: text ?? "", attributes: textAttributes) 80 | } 81 | 82 | override func draw(_ rect: CGRect) { 83 | let color = tintColor! 84 | color.setFill() 85 | 86 | let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius) 87 | path.fill() 88 | 89 | guard let context = UIGraphicsGetCurrentContext(), (text?.count ?? 0) > 0 else { 90 | return 91 | } 92 | 93 | let attributedString = self.attributedString 94 | let stringSize = attributedString.size() 95 | 96 | let xOrigin: CGFloat = max(horizontalInset, (rect.width - stringSize.width)/2) 97 | let yOrigin: CGFloat = max(verticalInset, (rect.height - stringSize.height)/2) 98 | 99 | context.saveGState() 100 | context.setBlendMode(.destinationOut) 101 | attributedString.draw(at: CGPoint(x: xOrigin, y: yOrigin)) 102 | context.restoreGState() 103 | } 104 | 105 | override func sizeThatFits(_ size: CGSize) -> CGSize { 106 | let stringSize = attributedString.size() 107 | return CGSize(width: stringSize.width + horizontalInset*2, height: stringSize.height + verticalInset*2) 108 | } 109 | 110 | override var intrinsicContentSize: CGSize { 111 | return sizeThatFits(.zero) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ImagePicker/CellRegistrator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellRegistrator.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 06/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Photos 12 | 13 | /// 14 | /// Convenient API to register custom cell classes or nibs for each item type. 15 | /// 16 | /// Supported item types: 17 | /// 1. action item - register a cell for all items or a different cell for each index. 18 | /// 2. camera item - register a subclass of `CameraCollectionViewCell` to provide a 19 | /// 3. asset item - each asset media type (image, video) can have it's own cell 20 | /// custom camera cell implementation. 21 | /// 22 | public final class CellRegistrator { 23 | 24 | deinit { 25 | log("deinit: \(String(describing: self))") 26 | } 27 | 28 | // MARK: Private Methods 29 | 30 | fileprivate let actionItemIdentifierPrefix = "eu.inloop.action-item.cell-id" 31 | fileprivate var actionItemNibsData: [Int: (UINib, String)]? 32 | fileprivate var actionItemClassesData: [Int: (UICollectionViewCell.Type, String)]? 33 | 34 | //camera item has only 1 cell so no need for identifiers 35 | fileprivate var cameraItemNib: UINib? 36 | fileprivate var cameraItemClass: UICollectionViewCell.Type? 37 | 38 | fileprivate let assetItemIdentifierPrefix = "eu.inloop.asset-item.cell-id" 39 | fileprivate var assetItemNibsData: [PHAssetMediaType: (UINib, String)]? 40 | fileprivate var assetItemClassesData: [PHAssetMediaType: (UICollectionViewCell.Type, String)]? 41 | 42 | //we use these if there is no asset media type specified 43 | fileprivate var assetItemNib: UINib? 44 | fileprivate var assetItemClass: UICollectionViewCell.Type? 45 | 46 | // MARK: Internal Methods 47 | 48 | let cellIdentifierForCameraItem = "eu.inloop.camera-item.cell-id" 49 | 50 | func cellIdentifier(forActionItemAt index: Int) -> String? { 51 | 52 | //first lets check if there is a registered cell at specified index 53 | if let index = actionItemNibsData?[index]?.1 ?? actionItemClassesData?[index]?.1 { 54 | return index 55 | } 56 | 57 | //if not found globaly registered return nil 58 | guard index < Int.max else { 59 | return nil 60 | } 61 | 62 | //lets see if there is a globally registered cell for all indexes 63 | return cellIdentifier(forActionItemAt: Int.max) 64 | } 65 | 66 | var hasUserRegisteredActionCell: Bool { 67 | return (actionItemNibsData?.count ?? 0) > 0 || (actionItemClassesData?.count ?? 0) > 0 68 | } 69 | 70 | var cellIdentifierForAssetItems: String { 71 | return assetItemIdentifierPrefix 72 | } 73 | 74 | func cellIdentifier(forAsset type: PHAssetMediaType) -> String? { 75 | return assetItemNibsData?[type]?.1 ?? assetItemClassesData?[type]?.1 76 | } 77 | 78 | // MARK: Public Methods 79 | 80 | /// 81 | /// Register a cell nib for all action items. Use this method if all action items 82 | /// have the same cell class. 83 | /// 84 | public func registerNibForActionItems(_ nib: UINib) { 85 | register(nib: nib, forActionItemAt: Int.max) 86 | } 87 | 88 | /// 89 | /// Register a cell class for all action items. Use this method if all action items 90 | /// have the same cell class. 91 | /// 92 | public func registerCellClassForActionItems(_ cellClass: UICollectionViewCell.Type) { 93 | register(cellClass: cellClass, forActionItemAt: Int.max) 94 | } 95 | 96 | /// 97 | /// Register a cell nib for an action item at particular index. Use this method if 98 | /// you wish to use different cells at each index. 99 | /// 100 | public func register(nib: UINib, forActionItemAt index: Int) { 101 | if actionItemNibsData == nil { 102 | actionItemNibsData = [:] 103 | } 104 | let cellIdentifier = actionItemIdentifierPrefix + String(index) 105 | actionItemNibsData?[index] = (nib, cellIdentifier) 106 | } 107 | 108 | /// 109 | /// Register a cell class for an action item at particular index. Use this method if 110 | /// you wish to use different cells at each index. 111 | /// 112 | public func register(cellClass: UICollectionViewCell.Type, forActionItemAt index: Int) { 113 | if actionItemClassesData == nil { 114 | actionItemClassesData = [:] 115 | } 116 | let cellIdentifier = actionItemIdentifierPrefix + String(index) 117 | actionItemClassesData?[index] = (cellClass, cellIdentifier) 118 | } 119 | 120 | /// 121 | /// Register a cell class for camera item. 122 | /// 123 | public func registerCellClassForCameraItem(_ cellClass: CameraCollectionViewCell.Type) { 124 | cameraItemClass = cellClass 125 | } 126 | 127 | /// 128 | /// Register a cell nib for camera item. 129 | /// 130 | /// - note: A cell class must subclass `CameraCollectionViewCell` or an exception 131 | /// will be thrown. 132 | /// 133 | public func registerNibForCameraItem(_ nib: UINib) { 134 | cameraItemNib = nib 135 | } 136 | 137 | /// 138 | /// Register a cell nib for asset items of specific type (image or video). 139 | /// 140 | /// - note: Please note, that if you register cell for specific type and your collection view displays 141 | /// also other types that you did not register an exception will be thrown. Always register cells 142 | /// for all media types you support. 143 | /// 144 | public func register(nib: UINib, forAssetItemOf type: PHAssetMediaType) { 145 | if assetItemNibsData == nil { 146 | assetItemNibsData = [:] 147 | } 148 | let cellIdentifier = assetItemIdentifierPrefix + String(describing: type.rawValue) 149 | assetItemNibsData?[type] = (nib, cellIdentifier) 150 | } 151 | 152 | /// 153 | /// Register a cell class for asset items of specific type (image or video). 154 | /// 155 | /// - note: Please note, that if you register cell for specific type and your collection view displays 156 | /// also other types that you did not register an exception will be thrown. Always register cells 157 | /// for all media types you support. 158 | /// 159 | public func register(cellClass: T.Type, forAssetItemOf type: PHAssetMediaType) where T: ImagePickerAssetCell { 160 | if assetItemClassesData == nil { 161 | assetItemClassesData = [:] 162 | } 163 | let cellIdentifier = assetItemIdentifierPrefix + String(describing: type.rawValue) 164 | assetItemClassesData?[type] = (cellClass, cellIdentifier) 165 | } 166 | 167 | /// 168 | /// Register a cell class for all asset items types (image and video). 169 | /// 170 | public func registerCellClassForAssetItems(_ cellClass: T.Type) where T: ImagePickerAssetCell { 171 | assetItemClass = cellClass 172 | } 173 | 174 | /// 175 | /// Register a cell nib for all asset items types (image and video). 176 | /// 177 | /// Please note that cell's class must conform to `ImagePickerAssetCell` protocol, otherwise an exception will be thrown. 178 | /// 179 | public func registerNibForAssetItems(_ nib: UINib) { 180 | assetItemNib = nib 181 | } 182 | } 183 | 184 | extension UICollectionView { 185 | 186 | /// 187 | /// Used by datasource when registering all cells to the collection view. If user 188 | /// did not register custom cells, this method registers default cells 189 | /// 190 | func apply(registrator: CellRegistrator, cameraMode: CaptureSettings.CameraMode) { 191 | 192 | //register action items considering type 193 | //if user did not register any nib or cell, use default action cell 194 | if registrator.hasUserRegisteredActionCell == false { 195 | registrator.registerCellClassForActionItems(ActionCell.self) 196 | guard let identifier = registrator.cellIdentifier(forActionItemAt: Int.max) else { 197 | fatalError("Image Picker: unable to register default action item cell") 198 | } 199 | let nib = UINib(nibName: "ActionCell", bundle: Bundle(for: ActionCell.self)) 200 | register(nib, forCellWithReuseIdentifier: identifier) 201 | } 202 | else { 203 | register(nibsData: registrator.actionItemNibsData?.map { $1 }) 204 | register(classData: registrator.actionItemClassesData?.map { $1 }) 205 | } 206 | 207 | //register camera item 208 | switch (registrator.cameraItemNib, registrator.cameraItemClass) { 209 | 210 | case (nil, nil): 211 | //if user does not set any class or nib we have to register default cell `CameraCollectionViewCell` based on camera mode 212 | switch cameraMode { 213 | case .photo, .photoAndLivePhoto: 214 | let nib = UINib(nibName: "LivePhotoCameraCell", bundle: Bundle(for: LivePhotoCameraCell.self)) 215 | register(nib, forCellWithReuseIdentifier: registrator.cellIdentifierForCameraItem) 216 | 217 | case .photoAndVideo: 218 | let nib = UINib(nibName: "VideoCameraCell", bundle: Bundle(for: VideoCameraCell.self)) 219 | register(nib, forCellWithReuseIdentifier: registrator.cellIdentifierForCameraItem) 220 | } 221 | 222 | case (let nib, nil): 223 | register(nib, forCellWithReuseIdentifier: registrator.cellIdentifierForCameraItem) 224 | 225 | case (_, let cellClass): 226 | register(cellClass, forCellWithReuseIdentifier: registrator.cellIdentifierForCameraItem) 227 | } 228 | 229 | //register asset items considering type 230 | register(nibsData: registrator.assetItemNibsData?.map { $1 }) 231 | register(classData: registrator.assetItemClassesData?.map { $1 }) 232 | 233 | //register asset items regardless of specified type 234 | switch (registrator.assetItemNib, registrator.assetItemClass) { 235 | 236 | case (nil, nil): 237 | //if user did not register all required classes/nibs - register default cells 238 | register(VideoAssetCell.self, forCellWithReuseIdentifier: registrator.cellIdentifierForAssetItems) 239 | //fatalError("there is not registered cell class nor nib for asset items, please user appropriate register methods on `CellRegistrator`") 240 | 241 | case (let nib, nil): 242 | register(nib, forCellWithReuseIdentifier: registrator.cellIdentifierForAssetItems) 243 | 244 | case (_, let cellClass): 245 | register(cellClass, forCellWithReuseIdentifier: registrator.cellIdentifierForAssetItems) 246 | } 247 | } 248 | 249 | /// 250 | /// Helper func that takes nib,cellid pair and registers them on a collection view 251 | /// 252 | fileprivate func register(nibsData: [(UINib, String)]?) { 253 | guard let nibsData = nibsData else { return } 254 | for (nib, cellIdentifier) in nibsData { 255 | register(nib, forCellWithReuseIdentifier: cellIdentifier) 256 | } 257 | } 258 | 259 | /// 260 | /// Helper func that takes nib,cellid pair and registers them on a collection view 261 | /// 262 | fileprivate func register(classData: [(UICollectionViewCell.Type, String)]?) { 263 | guard let classData = classData else { return } 264 | for (cellType, cellIdentifier) in classData { 265 | register(cellType, forCellWithReuseIdentifier: cellIdentifier) 266 | } 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /ImagePicker/CollectionViewBatchAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewBatchAnimation.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 13/04/2018. 6 | // Copyright © 2018 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | /// 13 | /// Wraps collectionView's `performBatchUpdates` block into AsynchronousOperation. 14 | /// 15 | final class CollectionViewBatchAnimation : AsynchronousOperation where ObjectType : PHObject { 16 | private let collectionView: UICollectionView 17 | private let sectionIndex: Int 18 | private let changes: PHFetchResultChangeDetails 19 | 20 | init(collectionView: UICollectionView, sectionIndex: Int, changes: PHFetchResultChangeDetails) { 21 | self.collectionView = collectionView 22 | self.sectionIndex = sectionIndex 23 | self.changes = changes 24 | } 25 | 26 | override func execute() { 27 | // If we have incremental diffs, animate them in the collection view 28 | collectionView.performBatchUpdates({ [unowned self] in 29 | 30 | // For indexes to make sense, updates must be in this order: 31 | // delete, insert, reload, move 32 | if let removed = self.changes.removedIndexes, removed.isEmpty == false { 33 | self.collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: self.sectionIndex) })) 34 | } 35 | if let inserted = changes.insertedIndexes, inserted.isEmpty == false { 36 | self.collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: self.sectionIndex) })) 37 | } 38 | if let changed = changes.changedIndexes, changed.isEmpty == false { 39 | self.collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: self.sectionIndex) })) 40 | } 41 | changes.enumerateMoves { fromIndex, toIndex in 42 | self.collectionView.moveItem(at: IndexPath(item: fromIndex, section: self.sectionIndex), to: IndexPath(item: toIndex, section: self.sectionIndex)) 43 | } 44 | }, completion: { finished in 45 | self.completeOperation() 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ImagePicker/CollectionViewUpdatesCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewUpdatesCoordinator.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 13/04/2018. 6 | // Copyright © 2018 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | /// 13 | /// Makes sure that all updates are performed in a serial queue, especially batch animations. This 14 | /// will make sure that reloadData() will never be called durring batch updates animations, which 15 | /// will prevent collection view from crashing on internal incosistency. 16 | /// 17 | final class CollectionViewUpdatesCoordinator { 18 | deinit { 19 | log("deinit: \(String(describing: self))") 20 | } 21 | 22 | private let collectionView: UICollectionView 23 | 24 | private var serialMainQueue: OperationQueue = { 25 | let queue = OperationQueue() 26 | queue.maxConcurrentOperationCount = 1 27 | queue.underlyingQueue = DispatchQueue.main 28 | return queue 29 | }() 30 | 31 | init(collectionView: UICollectionView) { 32 | self.collectionView = collectionView 33 | } 34 | 35 | /// Provides opportunuty to update collectionView's dataSource in underlaying queue. 36 | func performDataSourceUpdate(updates: @escaping ()->Void) { 37 | serialMainQueue.addOperation(updates) 38 | } 39 | 40 | /// Updates collection view. 41 | func performChanges(_ changes: PHFetchResultChangeDetails, inSection: Int) { 42 | if changes.hasIncrementalChanges { 43 | let operation = CollectionViewBatchAnimation(collectionView: collectionView, sectionIndex: inSection, changes: changes) 44 | serialMainQueue.addOperation(operation) 45 | } 46 | else { 47 | serialMainQueue.addOperation { [unowned self] in 48 | self.collectionView.reloadData() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ImagePicker/ImagePicker.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.h 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 04/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ImagePicker. 12 | FOUNDATION_EXPORT double ImagePickerVersionNumber; 13 | 14 | //! Project version string for ImagePicker. 15 | FOUNDATION_EXPORT const unsigned char ImagePickerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #import "UIImageEffects.h" 20 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerAssetModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerAssetModel.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 12/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | /// 13 | /// Model that is used when accessing an caching PHAsset objects 14 | /// 15 | final class ImagePickerAssetModel { 16 | 17 | deinit { 18 | log("deinit: \(String(describing: self))") 19 | } 20 | 21 | var fetchResult: PHFetchResult! { 22 | set { userDefinedFetchResult = newValue } 23 | get { return userDefinedFetchResult ?? defaultFetchResult } 24 | } 25 | 26 | lazy var imageManager = PHCachingImageManager() 27 | var thumbnailSize: CGSize? 28 | 29 | /// Tryies to access smart album .smartAlbumUserLibrary that should be `Camera Roll` and uses just fetchAssets as fallback 30 | private lazy var defaultFetchResult: PHFetchResult = { 31 | 32 | let assetsOptions = PHFetchOptions() 33 | assetsOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 34 | assetsOptions.fetchLimit = 1000 35 | 36 | let collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil) 37 | if let cameraRoll = collections.firstObject { 38 | return PHAsset.fetchAssets(in: cameraRoll, options: assetsOptions) 39 | } 40 | else { 41 | return PHAsset.fetchAssets(with: assetsOptions) 42 | } 43 | }() 44 | 45 | private var userDefinedFetchResult: PHFetchResult? 46 | 47 | //will be use for caching 48 | var previousPreheatRect = CGRect.zero 49 | 50 | func updateCachedAssets(collectionView: UICollectionView) { 51 | 52 | // Paradoxly, using this precaching the scrolling of images is more laggy than if there is no precaching 53 | 54 | guard let thumbnailSize = thumbnailSize else { 55 | return log("asset model: update cache assets - thumbnail size is nil") 56 | } 57 | 58 | guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { 59 | return log("asset model: update cache assets - collection view layout is not flow layout") 60 | } 61 | 62 | // The preheat window is twice the height of the visible rect. 63 | let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size) 64 | 65 | var preheatRect: CGRect 66 | 67 | switch layout.scrollDirection { 68 | case .vertical: 69 | 70 | preheatRect = visibleRect.insetBy(dx: 0, dy: -0.75 * visibleRect.height) 71 | 72 | // Update only if the visible area is significantly different from the last preheated area. 73 | let delta = abs(preheatRect.midY - previousPreheatRect.midY) 74 | guard delta > collectionView.bounds.height / 3 else { 75 | return 76 | } 77 | 78 | case .horizontal: 79 | 80 | preheatRect = visibleRect.insetBy(dx: -0.75 * visibleRect.width, dy: 0) 81 | 82 | // Update only if the visible area is significantly different from the last preheated area. 83 | let delta = abs(preheatRect.midX - previousPreheatRect.midX) 84 | guard delta > collectionView.bounds.width / 3 else { 85 | return 86 | } 87 | } 88 | 89 | // Compute the assets to start caching and to stop caching. 90 | let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect, layout.scrollDirection) 91 | let addedAssets = addedRects 92 | .flatMap { rect in collectionView.indexPathsForElements(in: rect) } 93 | .map { indexPath in fetchResult.object(at: indexPath.item) } 94 | let removedAssets = removedRects 95 | .flatMap { rect in collectionView.indexPathsForElements(in: rect) } 96 | .map { indexPath in fetchResult.object(at: indexPath.item) } 97 | 98 | // Update the assets the PHCachingImageManager is caching. 99 | imageManager.startCachingImages(for: addedAssets, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil) 100 | log("asset model: caching, size \(thumbnailSize), preheat rect \(preheatRect), items \(addedAssets.count)") 101 | 102 | imageManager.stopCachingImages(for: removedAssets, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil) 103 | log("asset model: uncaching, preheat rect \(preheatRect), items \(removedAssets.count)") 104 | 105 | // Store the preheat rect to compare against in the future. 106 | previousPreheatRect = preheatRect 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerDataSource.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 04/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | /// 13 | /// Datasource for a collection view that is used by Image Picker VC. 14 | /// 15 | final class ImagePickerDataSource : NSObject, UICollectionViewDataSource { 16 | 17 | deinit { 18 | log("deinit: \(String(describing: self))") 19 | } 20 | 21 | var layoutModel = LayoutModel.empty 22 | var cellRegistrator: CellRegistrator? 23 | var assetsModel: ImagePickerAssetModel 24 | 25 | init(assetsModel: ImagePickerAssetModel) { 26 | self.assetsModel = assetsModel 27 | super.init() 28 | } 29 | 30 | func numberOfSections(in collectionView: UICollectionView) -> Int { 31 | return layoutModel.numberOfSections 32 | } 33 | 34 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 35 | return layoutModel.numberOfItems(in: section) 36 | } 37 | 38 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 39 | 40 | guard let cellsRegistrator = cellRegistrator else { 41 | fatalError("cells registrator must be set at this moment") 42 | } 43 | 44 | //TODO: change these hardcoded section numbers to those defined in layoutModel.layoutConfiguration 45 | switch indexPath.section { 46 | case 0: 47 | guard let id = cellsRegistrator.cellIdentifier(forActionItemAt: indexPath.row) else { 48 | fatalError("there is an action item at index \(indexPath.row) but no cell is registered.") 49 | } 50 | return collectionView.dequeueReusableCell(withReuseIdentifier: id, for: indexPath) 51 | 52 | case 1: 53 | let id = cellsRegistrator.cellIdentifierForCameraItem 54 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: id, for: indexPath) as? CameraCollectionViewCell else { 55 | fatalError("there is a camera item but no cell class `CameraCollectionViewCell` is registered.") 56 | } 57 | return cell 58 | 59 | case 2: 60 | 61 | let asset = assetsModel.fetchResult.object(at: indexPath.item) 62 | let cellId = cellsRegistrator.cellIdentifier(forAsset: asset.mediaType) ?? cellsRegistrator.cellIdentifierForAssetItems 63 | 64 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as? ImagePickerAssetCell else { 65 | fatalError("asset item cell must conform to \(ImagePickerAssetCell.self) protocol") 66 | } 67 | 68 | let thumbnailSize = assetsModel.thumbnailSize ?? .zero 69 | 70 | // Request an image for the asset from the PHCachingImageManager. 71 | cell.representedAssetIdentifier = asset.localIdentifier 72 | assetsModel.imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { image, _ in 73 | // The cell may have been recycled by the time this handler gets called; 74 | // set the cell's thumbnail image only if it's still showing the same asset. 75 | if cell.representedAssetIdentifier == asset.localIdentifier && image != nil { 76 | cell.imageView.image = image 77 | } 78 | }) 79 | 80 | return cell as! UICollectionViewCell 81 | 82 | default: fatalError("only 3 sections are supported") 83 | } 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerDelegate.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 04/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Informs a delegate what is going on in ImagePickerDelegate 12 | protocol ImagePickerDelegateDelegate : class { 13 | 14 | /// Called when user selects one of action items 15 | func imagePicker(delegate: ImagePickerDelegate, didSelectActionItemAt index: Int) 16 | 17 | /// Called when user selects one of asset items 18 | func imagePicker(delegate: ImagePickerDelegate, didSelectAssetItemAt index: Int) 19 | 20 | /// Called when user deselects one of selected asset items 21 | func imagePicker(delegate: ImagePickerDelegate, didDeselectAssetItemAt index: Int) 22 | 23 | /// Called when action item is about to be displayed 24 | func imagePicker(delegate: ImagePickerDelegate, willDisplayActionCell cell: UICollectionViewCell, at index: Int) 25 | 26 | /// Called when camera item is about to be displayed 27 | func imagePicker(delegate: ImagePickerDelegate, willDisplayCameraCell cell: CameraCollectionViewCell) 28 | 29 | /// Called when camera item ended displaying 30 | func imagePicker(delegate: ImagePickerDelegate, didEndDisplayingCameraCell cell: CameraCollectionViewCell) 31 | 32 | func imagePicker(delegate: ImagePickerDelegate, willDisplayAssetCell cell: ImagePickerAssetCell, at index: Int) 33 | 34 | //func imagePicker(delegate: ImagePickerDelegate, didEndDisplayingAssetCell cell: ImagePickerAssetCell) 35 | func imagePicker(delegate: ImagePickerDelegate, didScroll scrollView: UIScrollView) 36 | } 37 | 38 | final class ImagePickerDelegate : NSObject, UICollectionViewDelegateFlowLayout { 39 | 40 | deinit { 41 | log("deinit: \(String(describing: self))") 42 | } 43 | 44 | var layout: ImagePickerLayout? 45 | weak var delegate: ImagePickerDelegateDelegate? 46 | 47 | private let selectionPolicy = ImagePickerSelectionPolicy() 48 | 49 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 50 | return layout?.collectionView(collectionView, layout: collectionViewLayout, sizeForItemAt: indexPath) ?? .zero 51 | } 52 | 53 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 54 | return layout?.collectionView(collectionView, layout: collectionViewLayout, insetForSectionAt: section) ?? .zero 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 58 | if indexPath.section == layout?.configuration.sectionIndexForAssets { 59 | delegate?.imagePicker(delegate: self, didSelectAssetItemAt: indexPath.row) 60 | } 61 | } 62 | 63 | func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 64 | if indexPath.section == layout?.configuration.sectionIndexForAssets { 65 | delegate?.imagePicker(delegate: self, didDeselectAssetItemAt: indexPath.row) 66 | } 67 | } 68 | 69 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 70 | guard let configuration = layout?.configuration else { return false } 71 | return selectionPolicy.shouldSelectItem(atSection: indexPath.section, layoutConfiguration: configuration) 72 | } 73 | 74 | func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { 75 | guard let configuration = layout?.configuration else { return false } 76 | return selectionPolicy.shouldHighlightItem(atSection: indexPath.section, layoutConfiguration: configuration) 77 | } 78 | 79 | func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) { 80 | if indexPath.section == layout?.configuration.sectionIndexForActions { 81 | delegate?.imagePicker(delegate: self, didSelectActionItemAt: indexPath.row) 82 | } 83 | } 84 | 85 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 86 | 87 | guard let configuration = layout?.configuration else { return } 88 | 89 | switch indexPath.section { 90 | case configuration.sectionIndexForActions: delegate?.imagePicker(delegate: self, willDisplayActionCell: cell, at: indexPath.row) 91 | case configuration.sectionIndexForCamera: delegate?.imagePicker(delegate: self, willDisplayCameraCell: cell as! CameraCollectionViewCell) 92 | case configuration.sectionIndexForAssets: delegate?.imagePicker(delegate: self, willDisplayAssetCell: cell as! ImagePickerAssetCell, at: indexPath.row) 93 | default: fatalError("index path not supported") 94 | } 95 | } 96 | 97 | func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 98 | 99 | guard let configuration = layout?.configuration else { return } 100 | 101 | switch indexPath.section { 102 | case configuration.sectionIndexForCamera: delegate?.imagePicker(delegate: self, didEndDisplayingCameraCell: cell as! CameraCollectionViewCell) 103 | case configuration.sectionIndexForActions, configuration.sectionIndexForAssets: break 104 | default: fatalError("index path not supported") 105 | } 106 | } 107 | 108 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 109 | delegate?.imagePicker(delegate: self, didScroll: scrollView) 110 | } 111 | 112 | @available(iOS 11.0, *) 113 | func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { 114 | log("XXX: \(scrollView.adjustedContentInset)") 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerLayout.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 05/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// A helper class that contains all code and logic when doing layout of collection 13 | /// view cells. This is used sollely by collection view's delegate. Typically 14 | /// this code should be part of regular subclass of UICollectionViewLayout, however, 15 | /// since we are using UICollectionViewFlowLayout we have to do this workaround. 16 | /// 17 | final class ImagePickerLayout { 18 | 19 | deinit { 20 | log("deinit: \(String(describing: self))") 21 | } 22 | 23 | var configuration: LayoutConfiguration 24 | 25 | init(configuration: LayoutConfiguration) { 26 | self.configuration = configuration 27 | } 28 | 29 | /// Returns size for item considering number of rows and scroll direction, if preferredWidthOrHeight is nil, square size is returned 30 | func sizeForItem(numberOfItemsInRow: Int, preferredWidthOrHeight: CGFloat?, collectionView: UICollectionView, scrollDirection: UICollectionViewScrollDirection) -> CGSize { 31 | 32 | switch scrollDirection { 33 | case .horizontal: 34 | var itemHeight = collectionView.frame.height 35 | itemHeight -= (collectionView.contentInset.top + collectionView.contentInset.bottom) 36 | itemHeight -= (CGFloat(numberOfItemsInRow) - 1) * configuration.interitemSpacing 37 | itemHeight /= CGFloat(numberOfItemsInRow) 38 | return CGSize(width: preferredWidthOrHeight ?? itemHeight, height: itemHeight) 39 | 40 | case .vertical: 41 | var itemWidth = collectionView.frame.width 42 | itemWidth -= (collectionView.contentInset.left + collectionView.contentInset.right) 43 | itemWidth -= (CGFloat(numberOfItemsInRow) - 1) * configuration.interitemSpacing 44 | itemWidth /= CGFloat(numberOfItemsInRow) 45 | return CGSize(width: itemWidth, height: preferredWidthOrHeight ?? itemWidth) 46 | } 47 | } 48 | 49 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 50 | 51 | guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { 52 | fatalError("currently only UICollectionViewFlowLayout is supported") 53 | } 54 | 55 | let layoutModel = LayoutModel(configuration: configuration, assets: 0) 56 | 57 | switch indexPath.section { 58 | case configuration.sectionIndexForActions: 59 | //this will make sure that action item is either square if there are 2 items, 60 | //or a recatangle if there is only 1 item 61 | //let width = sizeForItem(numberOfItemsInRow: 2, preferredWidthOrHeight: nil, collectionView: collectionView, scrollDirection: layout.scrollDirection).width 62 | let ratio: CGFloat = 0.25 63 | let width = collectionView.frame.width * ratio 64 | return sizeForItem(numberOfItemsInRow: layoutModel.numberOfItems(in: configuration.sectionIndexForActions), preferredWidthOrHeight: width, collectionView: collectionView, scrollDirection: layout.scrollDirection) 65 | 66 | case configuration.sectionIndexForCamera: 67 | //lets keep this ratio so camera item is a nice rectangle 68 | 69 | let traitCollection = collectionView.traitCollection 70 | 71 | var ratio: CGFloat = 160/212 72 | 73 | //for iphone in landscape we need different ratio 74 | switch traitCollection.userInterfaceIdiom { 75 | case .phone: 76 | switch (traitCollection.horizontalSizeClass, traitCollection.verticalSizeClass) { 77 | //iphones in landscape 78 | case (.unspecified, .compact): 79 | fallthrough 80 | //iphones+ in landscape 81 | case (.regular, .compact): 82 | fallthrough 83 | //iphones in landscape except iphone + 84 | case (.compact, .compact): 85 | ratio = 1/ratio 86 | default: break 87 | } 88 | 89 | default: 90 | break 91 | } 92 | 93 | let widthOrHeight: CGFloat = collectionView.frame.height * ratio 94 | return sizeForItem(numberOfItemsInRow: layoutModel.numberOfItems(in: configuration.sectionIndexForCamera), preferredWidthOrHeight: widthOrHeight, collectionView: collectionView, scrollDirection: layout.scrollDirection) 95 | 96 | case configuration.sectionIndexForAssets: 97 | //make sure there is at least 1 item, othewise invalid layout 98 | assert(configuration.numberOfAssetItemsInRow > 0, "invalid layout - numberOfAssetItemsInRow must be > 0, check your layout configuration ") 99 | return sizeForItem(numberOfItemsInRow: configuration.numberOfAssetItemsInRow, preferredWidthOrHeight: nil, collectionView: collectionView, scrollDirection: layout.scrollDirection) 100 | 101 | default: 102 | fatalError("unexpected sections count") 103 | } 104 | 105 | } 106 | 107 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 108 | 109 | guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { 110 | fatalError("currently only UICollectionViewFlowLayout is supported") 111 | } 112 | 113 | /// helper method that creates edge insets considering scroll direction 114 | func sectionInsets(_ inset: CGFloat) -> UIEdgeInsets { 115 | switch layout.scrollDirection { 116 | case .horizontal: return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: inset) 117 | case .vertical: return UIEdgeInsets(top: 0, left: 0, bottom: inset, right: 0) 118 | } 119 | } 120 | 121 | let layoutModel = LayoutModel(configuration: configuration, assets: 0) 122 | 123 | switch section { 124 | case 0 where layoutModel.numberOfItems(in: section) > 0: 125 | return sectionInsets(configuration.actionSectionSpacing) 126 | case 1 where layoutModel.numberOfItems(in: section) > 0: 127 | return sectionInsets(configuration.cameraSectionSpacing) 128 | default: 129 | return .zero 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerSelectionPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerSelectionPolicy.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 06/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// Helper class that determines which cells are selected, multiple selected or 13 | /// highlighted. 14 | /// 15 | /// We allow selecting only asset items, action items are only highlighted, 16 | /// camera item is untouched. 17 | /// 18 | struct ImagePickerSelectionPolicy { 19 | 20 | func shouldSelectItem(atSection section: Int, layoutConfiguration: LayoutConfiguration) -> Bool { 21 | switch section { 22 | case layoutConfiguration.sectionIndexForActions, layoutConfiguration.sectionIndexForCamera: 23 | return false 24 | default: 25 | return true 26 | } 27 | } 28 | 29 | func shouldHighlightItem(atSection section: Int, layoutConfiguration: LayoutConfiguration) -> Bool { 30 | switch section { 31 | case layoutConfiguration.sectionIndexForCamera: 32 | return false 33 | default: 34 | return true 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerView.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 07/11/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ImagePickerView : UIView { 12 | 13 | @IBOutlet weak var collectionView: UICollectionView! 14 | 15 | } 16 | -------------------------------------------------------------------------------- /ImagePicker/ImagePickerView.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /ImagePicker/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 | FMWK 17 | CFBundleShortVersionString 18 | 0.7 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ImagePicker/LayoutConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutConfiguration.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 05/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// A helper struct that is used by ImagePickerLayout when configuring and laying out 13 | /// collection view items. 14 | /// 15 | public struct LayoutConfiguration { 16 | 17 | public var showsFirstActionItem = true 18 | public var showsSecondActionItem = true 19 | 20 | public var showsCameraItem = true 21 | 22 | let showsAssetItems = true 23 | 24 | /// 25 | /// Scroll and layout direction 26 | /// 27 | public var scrollDirection: UICollectionViewScrollDirection = .horizontal 28 | 29 | /// 30 | /// Defines how many image assets will be in a row. Must be > 0 31 | /// 32 | public var numberOfAssetItemsInRow: Int = 2 33 | 34 | /// 35 | /// Spacing between items within a section 36 | /// 37 | public var interitemSpacing: CGFloat = 1 38 | 39 | /// 40 | /// Spacing between actions section and camera section 41 | /// 42 | public var actionSectionSpacing: CGFloat = 1 43 | 44 | /// 45 | /// Spacing between camera section and assets section 46 | /// 47 | public var cameraSectionSpacing: CGFloat = 10 48 | } 49 | 50 | extension LayoutConfiguration { 51 | 52 | var hasAnyAction: Bool { 53 | return showsFirstActionItem || showsSecondActionItem 54 | } 55 | 56 | var sectionIndexForActions: Int { 57 | return 0 58 | } 59 | 60 | var sectionIndexForCamera: Int { 61 | return 1 62 | } 63 | 64 | var sectionIndexForAssets: Int { 65 | return 2 66 | } 67 | 68 | public static var `default` = LayoutConfiguration() 69 | 70 | } 71 | 72 | extension UICollectionView { 73 | 74 | /// Helper method for convenienet access to camera cell 75 | func cameraCell(layout: LayoutConfiguration) -> CameraCollectionViewCell? { 76 | return cellForItem(at: IndexPath(row: 0, section: layout.sectionIndexForCamera)) as? CameraCollectionViewCell 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /ImagePicker/LayoutModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutModel.swift 3 | // Image Picker 4 | // 5 | // Created by Peter Stajger on 05/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// A model that contains info that is used by layout code and collection view data source 13 | /// when figuring out layout structure. 14 | /// 15 | /// Always contains 3 sections: 16 | /// 1. for actions (supports up to 2 action items) 17 | /// 2. for camera (1 camera item) 18 | /// 3. for image assets (any number of image asset items) 19 | /// Each section can be empty. 20 | /// 21 | struct LayoutModel { 22 | 23 | private var sections: [Int] = [0, 0, 0] 24 | 25 | init(configuration: LayoutConfiguration, assets: Int) { 26 | var actionItems: Int = configuration.showsFirstActionItem ? 1 : 0 27 | actionItems += configuration.showsSecondActionItem ? 1 : 0 28 | sections[configuration.sectionIndexForActions] = actionItems 29 | sections[configuration.sectionIndexForCamera] = configuration.showsCameraItem ? 1 : 0 30 | sections[configuration.sectionIndexForAssets] = assets 31 | } 32 | 33 | var numberOfSections: Int { 34 | return sections.count 35 | } 36 | 37 | func numberOfItems(in section: Int) -> Int { 38 | return sections[section] 39 | } 40 | 41 | static var empty: LayoutModel { 42 | let emptyConfiguration = LayoutConfiguration(showsFirstActionItem: false, showsSecondActionItem: false, showsCameraItem: false, scrollDirection: .horizontal, numberOfAssetItemsInRow: 0, interitemSpacing: 0, actionSectionSpacing: 0, cameraSectionSpacing: 0) 43 | return LayoutModel(configuration: emptyConfiguration, assets: 0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ImagePicker/LivePhotoCameraCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LivePhotoCameraCell.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 25/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class LivePhotoCameraCell : CameraCollectionViewCell { 13 | 14 | @IBOutlet weak var snapButton: UIButton! 15 | @IBOutlet weak var enableLivePhotosButton: StationaryButton! 16 | @IBOutlet weak var liveIndicator: CarvedLabel! 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | liveIndicator.alpha = 0 21 | liveIndicator.tintColor = UIColor(red: 245/255, green: 203/255, blue: 47/255, alpha: 1) 22 | 23 | enableLivePhotosButton.unselectedTintColor = UIColor.white 24 | enableLivePhotosButton.selectedTintColor = UIColor(red: 245/255, green: 203/255, blue: 47/255, alpha: 1) 25 | } 26 | 27 | @IBAction func snapButtonTapped(_ sender: UIButton) { 28 | if enableLivePhotosButton.isSelected { 29 | takeLivePhoto() 30 | } 31 | else { 32 | takePicture() 33 | } 34 | } 35 | 36 | @IBAction func flipButtonTapped(_ sender: UIButton) { 37 | flipCamera() 38 | } 39 | 40 | func updateWithCameraMode(_ mode: CaptureSettings.CameraMode) { 41 | switch mode { 42 | case .photo: 43 | liveIndicator.isHidden = true 44 | enableLivePhotosButton.isHidden = true 45 | case .photoAndLivePhoto: 46 | liveIndicator.isHidden = false 47 | enableLivePhotosButton.isHidden = false 48 | default: 49 | fatalError("Image Picker - unsupported camera mode for \(type(of: self))") 50 | } 51 | } 52 | 53 | // MARK: Override Methods 54 | 55 | override func updateLivePhotoStatus(isProcessing: Bool, shouldAnimate: Bool) { 56 | 57 | let updates: () -> Void = { 58 | self.liveIndicator.alpha = isProcessing ? 1 : 0 59 | } 60 | 61 | shouldAnimate ? UIView.animate(withDuration: 0.25, animations: updates) : updates() 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /ImagePicker/LivePhotoCameraCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 32 | 43 | 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 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /ImagePicker/Miscellaneous.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 19/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | func log(_ message: String) { 13 | #if DEBUG 14 | debugPrint(message) 15 | #endif 16 | } 17 | 18 | extension UICollectionView { 19 | 20 | func indexPathsForElements(in rect: CGRect) -> [IndexPath] { 21 | let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)! 22 | let paths = allLayoutAttributes.map { $0.indexPath } 23 | return paths 24 | } 25 | 26 | } 27 | 28 | extension UIInterfaceOrientation : CustomDebugStringConvertible { 29 | 30 | public var debugDescription: String { 31 | switch self { 32 | case .unknown: return "unknown" 33 | case .portrait: return "portrait" 34 | case .portraitUpsideDown: return "portrait upside down" 35 | case .landscapeRight: return "landscape right" 36 | case .landscapeLeft: return "landscape left" 37 | } 38 | } 39 | 40 | } 41 | 42 | func differencesBetweenRects(_ old: CGRect, _ new: CGRect, _ scrollDirection: UICollectionViewScrollDirection) -> (added: [CGRect], removed: [CGRect]) { 43 | switch scrollDirection { 44 | case .horizontal: return differencesBetweenRectsHorizontal(old, new) 45 | case .vertical: return differencesBetweenRectsVertical(old, new) 46 | } 47 | } 48 | 49 | func differencesBetweenRectsVertical(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) { 50 | if old.intersects(new) { 51 | var added = [CGRect]() 52 | if new.maxY > old.maxY { 53 | added += [CGRect(x: new.origin.x, y: old.maxY, width: new.width, height: new.maxY - old.maxY)] 54 | } 55 | if old.minY > new.minY { 56 | added += [CGRect(x: new.origin.x, y: new.minY, width: new.width, height: old.minY - new.minY)] 57 | } 58 | var removed = [CGRect]() 59 | if new.maxY < old.maxY { 60 | removed += [CGRect(x: new.origin.x, y: new.maxY, width: new.width, height: old.maxY - new.maxY)] 61 | } 62 | if old.minY < new.minY { 63 | removed += [CGRect(x: new.origin.x, y: old.minY, width: new.width, height: new.minY - old.minY)] 64 | } 65 | return (added, removed) 66 | } 67 | else { 68 | return ([new], [old]) 69 | } 70 | } 71 | 72 | func differencesBetweenRectsHorizontal(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) { 73 | if old.intersects(new) { 74 | var added = [CGRect]() 75 | if new.maxX > old.maxX { 76 | added += [CGRect(x: old.maxX, y: old.origin.y, width: new.maxX - old.maxX, height: old.height)] 77 | } 78 | if old.minX > new.minX { 79 | added += [CGRect(x: new.minX, y: old.origin.y, width: old.maxX - new.maxX, height: old.height)] 80 | } 81 | var removed = [CGRect]() 82 | if new.maxX < old.maxX { 83 | removed += [CGRect(x: new.maxX, y: old.origin.y, width: old.maxX - new.maxX, height: old.height)] 84 | } 85 | if old.minX < new.minX { 86 | removed += [CGRect(x: old.minX, y: old.origin.y, width: new.maxX - old.maxX, height: old.height)] 87 | } 88 | return (added, removed) 89 | } 90 | else { 91 | return ([new], [old]) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ImagePicker/PhotoCaptureDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | Photo capture delegate. 7 | */ 8 | 9 | import AVFoundation 10 | import Photos 11 | 12 | final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate { 13 | 14 | deinit { 15 | log("deinit: \(String(describing: self))") 16 | } 17 | 18 | // MARK: Public Methods 19 | 20 | /// set this to false if you dont wish to save taken picture to photo library 21 | var savesPhotoToLibrary = true 22 | 23 | /// this contains photo data when taken 24 | private(set) var photoData: Data? = nil 25 | 26 | private(set) var requestedPhotoSettings: AVCapturePhotoSettings 27 | 28 | /// not nil if error occured during capturing 29 | private(set) var processError: Error? 30 | 31 | // MARK: Private Methods 32 | 33 | private let willCapturePhotoAnimation: () -> () 34 | private let capturingLivePhoto: (Bool) -> () 35 | private let completed: (PhotoCaptureDelegate) -> () 36 | private var livePhotoCompanionMovieURL: URL? = nil 37 | 38 | init(with requestedPhotoSettings: AVCapturePhotoSettings, willCapturePhotoAnimation: @escaping () -> (), capturingLivePhoto: @escaping (Bool) -> (), completed: @escaping (PhotoCaptureDelegate) -> ()) { 39 | self.requestedPhotoSettings = requestedPhotoSettings 40 | self.willCapturePhotoAnimation = willCapturePhotoAnimation 41 | self.capturingLivePhoto = capturingLivePhoto 42 | self.completed = completed 43 | } 44 | 45 | private func didFinish() { 46 | if let livePhotoCompanionMoviePath = livePhotoCompanionMovieURL?.path { 47 | if FileManager.default.fileExists(atPath: livePhotoCompanionMoviePath) { 48 | do { 49 | try FileManager.default.removeItem(atPath: livePhotoCompanionMoviePath) 50 | } 51 | catch { 52 | log("photo capture delegate: Could not remove file at url: \(livePhotoCompanionMoviePath)") 53 | } 54 | } 55 | } 56 | 57 | completed(self) 58 | } 59 | 60 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) { 61 | if resolvedSettings.livePhotoMovieDimensions.width > 0 && resolvedSettings.livePhotoMovieDimensions.height > 0 { 62 | capturingLivePhoto(true) 63 | } 64 | } 65 | 66 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) { 67 | willCapturePhotoAnimation() 68 | } 69 | 70 | // TODO: lets for now use the older deprecated method 71 | // @available(iOS 11.0, *) 72 | // func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { 73 | // if let data = photo.fileDataRepresentation() { 74 | // photoData = data 75 | // } 76 | // else if let error = error { 77 | // log("photo capture delegate: error capturing photo: \(error)") 78 | // processError = error 79 | // return 80 | // } 81 | // } 82 | 83 | //this method is not called on iOS 11 if method above is implemented 84 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) { 85 | if let photoSampleBuffer = photoSampleBuffer { 86 | photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer) 87 | } 88 | else if let error = error { 89 | log("photo capture delegate: error capturing photo: \(error)") 90 | processError = error 91 | return 92 | } 93 | } 94 | 95 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL, resolvedSettings: AVCaptureResolvedPhotoSettings) { 96 | capturingLivePhoto(false) 97 | } 98 | 99 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL, duration: CMTime, photoDisplayTime: CMTime, resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) { 100 | if let error = error { 101 | log("photo capture delegate: error processing live photo companion movie: \(error)") 102 | return 103 | } 104 | 105 | livePhotoCompanionMovieURL = outputFileURL 106 | } 107 | 108 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) { 109 | 110 | if let error = error { 111 | log("photo capture delegate: Error capturing photo: \(error)") 112 | didFinish() 113 | return 114 | } 115 | 116 | guard let photoData = photoData else { 117 | log("photo capture delegate: No photo data resource") 118 | didFinish() 119 | return 120 | } 121 | 122 | guard savesPhotoToLibrary == true else { 123 | log("photo capture delegate: photo did finish without saving to photo library") 124 | didFinish() 125 | return 126 | } 127 | 128 | PHPhotoLibrary.requestAuthorization { [unowned self] status in 129 | if status == .authorized { 130 | PHPhotoLibrary.shared().performChanges({ [unowned self] in 131 | let creationRequest = PHAssetCreationRequest.forAsset() 132 | creationRequest.addResource(with: .photo, data: photoData, options: nil) 133 | 134 | if let livePhotoCompanionMovieURL = self.livePhotoCompanionMovieURL { 135 | let livePhotoCompanionMovieFileResourceOptions = PHAssetResourceCreationOptions() 136 | livePhotoCompanionMovieFileResourceOptions.shouldMoveFile = true 137 | creationRequest.addResource(with: .pairedVideo, fileURL: livePhotoCompanionMovieURL, options: livePhotoCompanionMovieFileResourceOptions) 138 | } 139 | 140 | }, completionHandler: { [unowned self] success, error in 141 | if let error = error { 142 | log("photo capture delegate: Error occurered while saving photo to photo library: \(error)") 143 | } 144 | 145 | self.didFinish() 146 | } 147 | ) 148 | } 149 | else { 150 | self.didFinish() 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /ImagePicker/RecordButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordButton.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 17/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// A rounded button with 2 circles where middle circle animates based on 13 | /// 3 states - initial, pressed, recording. 14 | /// 15 | class RecordVideoButton : StationaryButton { 16 | 17 | var outerBorderWidth: CGFloat = 3 { didSet { setNeedsUpdateCircleLayers() } } 18 | var innerBorderWidth: CGFloat = 1.5 { didSet { setNeedsUpdateCircleLayers() } } 19 | var pressDepthFactor: CGFloat = 0.9 { didSet { setNeedsUpdateCircleLayers() } } 20 | 21 | override var isHighlighted: Bool { 22 | get { return super.isHighlighted } 23 | set { 24 | if isSelected == false && newValue != isHighlighted && newValue == true { 25 | updateCircleLayers(state: .pressed, animated: true) 26 | } 27 | super.isHighlighted = newValue 28 | } 29 | } 30 | 31 | override func selectionDidChange(animated: Bool) { 32 | super.selectionDidChange(animated: animated) 33 | 34 | if isSelected { 35 | updateCircleLayers(state: .recording, animated: animated) 36 | } 37 | else { 38 | updateCircleLayers(state: .initial, animated: animated) 39 | } 40 | } 41 | 42 | private var innerCircleLayerInset: CGFloat { 43 | return outerBorderWidth + innerBorderWidth 44 | } 45 | 46 | private var needsUpdateCircleLayers = true 47 | private var outerCircleLayer: CALayer 48 | private var innerCircleLayer: CALayer 49 | 50 | private enum LayersState: String { 51 | case initial 52 | case pressed 53 | case recording 54 | } 55 | 56 | private var layersState: LayersState = .initial 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | outerCircleLayer = CALayer() 60 | innerCircleLayer = CALayer() 61 | super.init(coder: aDecoder) 62 | backgroundColor = UIColor.clear 63 | layer.addSublayer(outerCircleLayer) 64 | layer.addSublayer(innerCircleLayer) 65 | CATransaction.setDisableActions(true) 66 | 67 | outerCircleLayer.backgroundColor = UIColor.clear.cgColor 68 | outerCircleLayer.cornerRadius = bounds.width/2 69 | outerCircleLayer.borderWidth = outerBorderWidth 70 | outerCircleLayer.borderColor = tintColor.cgColor 71 | 72 | innerCircleLayer.backgroundColor = UIColor.red.cgColor 73 | 74 | CATransaction.commit() 75 | } 76 | 77 | override func layoutSubviews() { 78 | super.layoutSubviews() 79 | if needsUpdateCircleLayers { 80 | CATransaction.setDisableActions(true) 81 | outerCircleLayer.frame = bounds 82 | innerCircleLayer.frame = bounds.insetBy(dx: innerCircleLayerInset, dy: innerCircleLayerInset) 83 | innerCircleLayer.cornerRadius = bounds.insetBy(dx: innerCircleLayerInset, dy: innerCircleLayerInset).width/2 84 | needsUpdateCircleLayers = false 85 | CATransaction.commit() 86 | } 87 | } 88 | 89 | private func setNeedsUpdateCircleLayers() { 90 | needsUpdateCircleLayers = true 91 | setNeedsLayout() 92 | } 93 | 94 | private func updateCircleLayers(state: LayersState, animated: Bool) { 95 | guard layersState != state else { return } 96 | 97 | layersState = state 98 | 99 | switch layersState { 100 | case .initial: 101 | setInnerLayer(recording: false, animated: animated) 102 | case .pressed: 103 | setInnerLayerPressed(animated: animated) 104 | case .recording: 105 | setInnerLayer(recording: true, animated: animated) 106 | } 107 | } 108 | 109 | private func setInnerLayerPressed(animated: Bool) { 110 | 111 | if animated { 112 | innerCircleLayer.add(transformAnimation(to: pressDepthFactor, duration: 0.25), forKey: nil) 113 | } 114 | else { 115 | CATransaction.setDisableActions(true) 116 | innerCircleLayer.setValue(pressDepthFactor, forKeyPath: "transform.scale") 117 | CATransaction.commit() 118 | } 119 | } 120 | 121 | private func setInnerLayer(recording: Bool, animated: Bool) { 122 | 123 | if recording { 124 | innerCircleLayer.add(transformAnimation(to: 0.5, duration: 0.15), forKey: nil) 125 | innerCircleLayer.cornerRadius = 8 126 | } 127 | else { 128 | innerCircleLayer.add(transformAnimation(to: 1, duration: 0.25), forKey: nil) 129 | innerCircleLayer.cornerRadius = bounds.insetBy(dx: innerCircleLayerInset, dy: innerCircleLayerInset).width/2 130 | } 131 | 132 | } 133 | 134 | private func transformAnimation(to value: CGFloat, duration: CFTimeInterval) -> CAAnimation { 135 | let animation = CABasicAnimation() 136 | animation.keyPath = "transform.scale" 137 | animation.fromValue = innerCircleLayer.presentation()?.value(forKeyPath: "transform.scale") 138 | animation.toValue = value 139 | animation.duration = duration 140 | animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 141 | animation.beginTime = CACurrentMediaTime() 142 | animation.fillMode = kCAFillModeForwards 143 | animation.isRemovedOnCompletion = false 144 | return animation 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /ImagePicker/RecordDurationLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordDurationLabel.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 25/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// Label that can be used to show duration during recording or just any 13 | /// duration in general. 14 | /// 15 | final class RecordDurationLabel : UILabel { 16 | 17 | private var indicatorLayer: CALayer = { 18 | let layer = CALayer() 19 | layer.masksToBounds = true 20 | layer.backgroundColor = UIColor(red: 234/255, green: 53/255, blue: 52/255, alpha: 1).cgColor 21 | layer.frame.size = CGSize(width: 6, height: 6) 22 | layer.cornerRadius = layer.frame.width/2 23 | layer.opacity = 0 //by default hidden 24 | return layer 25 | }() 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | commonInit() 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | commonInit() 35 | } 36 | 37 | override func layoutSubviews() { 38 | super.layoutSubviews() 39 | indicatorLayer.position = CGPoint(x: -7, y: bounds.height/2) 40 | } 41 | 42 | // MARK: Public Methods 43 | 44 | private var backingSeconds: TimeInterval = 10000 { 45 | didSet { 46 | updateLabel() 47 | } 48 | } 49 | 50 | func start() { 51 | 52 | guard secondTimer == nil else { 53 | return 54 | } 55 | 56 | secondTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] (timer) in 57 | self?.backingSeconds += 1 58 | }) 59 | secondTimer?.tolerance = 0.1 60 | 61 | indicatorTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] (timer) in 62 | self?.updateIndicator(appearDelay: 0.2) 63 | }) 64 | indicatorTimer?.tolerance = 0.1 65 | 66 | updateIndicator(appearDelay: 0) 67 | } 68 | 69 | func stop() { 70 | secondTimer?.invalidate() 71 | secondTimer = nil 72 | backingSeconds = 0 73 | updateLabel() 74 | 75 | indicatorTimer?.invalidate() 76 | indicatorTimer = nil 77 | indicatorLayer.removeAllAnimations() 78 | indicatorLayer.opacity = 0 79 | } 80 | 81 | // MARK: Private Methods 82 | 83 | private var secondTimer: Timer? 84 | private var indicatorTimer: Timer? 85 | 86 | private func updateLabel() { 87 | 88 | //we are not using DateComponentsFormatter because it does not pad zero to hours component 89 | //so it regurns pattern 0:00:00, we need 00:00:00 90 | let hours = Int(backingSeconds) / 3600 91 | let minutes = Int(backingSeconds) / 60 % 60 92 | let seconds = Int(backingSeconds) % 60 93 | text = String(format:"%02i:%02i:%02i", hours, minutes, seconds) 94 | } 95 | 96 | private func updateIndicator(appearDelay: CFTimeInterval = 0) { 97 | 98 | let disappearDelay: CFTimeInterval = 0.25 99 | 100 | let appear = appearAnimation(delay: appearDelay) 101 | let disappear = disappearAnimation(delay: appear.beginTime + appear.duration + disappearDelay) 102 | 103 | let animation = CAAnimationGroup() 104 | animation.animations = [appear, disappear] 105 | animation.duration = appear.duration + disappear.duration + appearDelay + disappearDelay 106 | animation.isRemovedOnCompletion = true 107 | 108 | indicatorLayer.add(animation, forKey: "blinkAnimationKey") 109 | } 110 | 111 | private func commonInit() { 112 | layer.addSublayer(indicatorLayer) 113 | clipsToBounds = false 114 | } 115 | 116 | private func appearAnimation(delay: CFTimeInterval = 0) -> CAAnimation { 117 | let appear = CABasicAnimation(keyPath: "opacity") 118 | appear.fromValue = indicatorLayer.presentation()?.opacity 119 | appear.toValue = 1 120 | appear.duration = 0.15 121 | appear.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 122 | appear.beginTime = delay 123 | appear.fillMode = kCAFillModeForwards 124 | return appear 125 | } 126 | 127 | private func disappearAnimation(delay: CFTimeInterval = 0) -> CAAnimation { 128 | let disappear = CABasicAnimation(keyPath: "opacity") 129 | disappear.fromValue = indicatorLayer.presentation()?.opacity 130 | disappear.toValue = 0 131 | disappear.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn) 132 | disappear.beginTime = delay 133 | disappear.duration = 0.25 134 | return disappear 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /ImagePicker/ShutterButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShutterButton.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 17/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// A rounded button that has a circle inside and is used when taking pictures. 13 | /// 14 | class ShutterButton : UIButton { 15 | 16 | var outerBorderWidth: CGFloat = 3 17 | var innerBorderWidth: CGFloat = 1.5 18 | var pressDepthFactor: CGFloat = 0.9 19 | 20 | override var isHighlighted: Bool { 21 | didSet { setInnerLayer(tapped: isHighlighted, animated: true) } 22 | } 23 | 24 | private var innerCircleLayerInset: CGFloat { 25 | return outerBorderWidth + innerBorderWidth 26 | } 27 | 28 | private var outerCircleLayer: CALayer 29 | private var innerCircleLayer: CALayer 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | outerCircleLayer = CALayer() 33 | innerCircleLayer = CALayer() 34 | super.init(coder: aDecoder) 35 | backgroundColor = UIColor.clear 36 | layer.addSublayer(outerCircleLayer) 37 | layer.addSublayer(innerCircleLayer) 38 | 39 | CATransaction.setDisableActions(true) 40 | 41 | outerCircleLayer.backgroundColor = UIColor.clear.cgColor 42 | outerCircleLayer.cornerRadius = bounds.width/2 43 | outerCircleLayer.borderWidth = outerBorderWidth 44 | outerCircleLayer.borderColor = tintColor.cgColor 45 | 46 | innerCircleLayer.backgroundColor = tintColor.cgColor 47 | 48 | CATransaction.commit() 49 | } 50 | 51 | func setInnerLayer(tapped: Bool, animated: Bool) { 52 | 53 | if animated { 54 | let animation = CABasicAnimation() 55 | animation.keyPath = "transform.scale" 56 | 57 | if tapped { 58 | animation.fromValue = innerCircleLayer.presentation()?.value(forKeyPath: "transform.scale") 59 | animation.toValue = pressDepthFactor 60 | animation.duration = 0.25 61 | } 62 | else { 63 | animation.fromValue = pressDepthFactor 64 | animation.toValue = 1.0 65 | animation.duration = 0.25 66 | } 67 | 68 | animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 69 | animation.beginTime = CACurrentMediaTime() 70 | animation.fillMode = kCAFillModeForwards 71 | animation.isRemovedOnCompletion = false 72 | 73 | innerCircleLayer.add(animation, forKey: nil) 74 | } 75 | else { 76 | CATransaction.setDisableActions(true) 77 | if tapped { 78 | innerCircleLayer.setValue(pressDepthFactor, forKeyPath: "transform.scale") 79 | } 80 | else { 81 | innerCircleLayer.setValue(CGFloat(1), forKeyPath: "transform.scale") 82 | } 83 | CATransaction.commit() 84 | } 85 | } 86 | 87 | override func layoutSubviews() { 88 | super.layoutSubviews() 89 | 90 | CATransaction.setDisableActions(true) 91 | outerCircleLayer.frame = bounds 92 | innerCircleLayer.frame = bounds.insetBy(dx: innerCircleLayerInset, dy: innerCircleLayerInset) 93 | innerCircleLayer.cornerRadius = bounds.insetBy(dx: innerCircleLayerInset, dy: innerCircleLayerInset).width/2 94 | CATransaction.commit() 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /ImagePicker/StationaryButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationaryButton.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 17/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// A button that keeps selected state when selected. 13 | /// 14 | class StationaryButton : UIButton { 15 | 16 | var unselectedTintColor: UIColor? 17 | var selectedTintColor: UIColor? 18 | 19 | open override var isSelected: Bool { 20 | get { return super.isSelected } 21 | set { setSelected(newValue, animated: false) } 22 | } 23 | 24 | open override var isHighlighted: Bool { 25 | didSet { 26 | if isHighlighted == false { 27 | setSelected(!isSelected, animated: true) 28 | } 29 | } 30 | } 31 | 32 | public func setSelected(_ selected: Bool, animated: Bool) { 33 | 34 | guard isSelected != selected else { 35 | return 36 | } 37 | 38 | super.isSelected = selected 39 | selectionDidChange(animated: animated) 40 | } 41 | 42 | open override func awakeFromNib() { 43 | super.awakeFromNib() 44 | updateTint() 45 | } 46 | 47 | /// 48 | /// Override this method to track when button's state is selected or deselected. 49 | /// You dont need to call super, default implementation does nothing. 50 | /// 51 | open func selectionDidChange(animated: Bool) { 52 | updateTint() 53 | } 54 | 55 | private func updateTint() { 56 | if isSelected { 57 | tintColor = selectedTintColor 58 | } 59 | else { 60 | tintColor = unselectedTintColor 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ImagePicker/UIImageEffects.h: -------------------------------------------------------------------------------- 1 | /* 2 | File: UIImageEffects.h 3 | Abstract: This class contains methods to apply blur and tint effects to an image. 4 | This is the code you’ll want to look out to find out how to use vImage to 5 | efficiently calculate a blur. 6 | Version: 1.1 7 | 8 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 9 | Inc. ("Apple") in consideration of your agreement to the following 10 | terms, and your use, installation, modification or redistribution of 11 | this Apple software constitutes acceptance of these terms. If you do 12 | not agree with these terms, please do not use, install, modify or 13 | redistribute this Apple software. 14 | 15 | In consideration of your agreement to abide by the following terms, and 16 | subject to these terms, Apple grants you a personal, non-exclusive 17 | license, under Apple's copyrights in this original Apple software (the 18 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 19 | Software, with or without modifications, in source and/or binary forms; 20 | provided that if you redistribute the Apple Software in its entirety and 21 | without modifications, you must retain this notice and the following 22 | text and disclaimers in all such redistributions of the Apple Software. 23 | Neither the name, trademarks, service marks or logos of Apple Inc. may 24 | be used to endorse or promote products derived from the Apple Software 25 | without specific prior written permission from Apple. Except as 26 | expressly stated in this notice, no other rights or licenses, express or 27 | implied, are granted by Apple herein, including but not limited to any 28 | patent rights that may be infringed by your derivative works or by other 29 | works in which the Apple Software may be incorporated. 30 | 31 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 32 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 33 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 34 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 35 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 36 | 37 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 38 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 39 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 40 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 41 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 42 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 43 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 44 | POSSIBILITY OF SUCH DAMAGE. 45 | 46 | Copyright (C) 2014 Apple Inc. All Rights Reserved. 47 | 48 | */ 49 | 50 | #import 51 | #import 52 | 53 | @interface UIImageEffects : NSObject 54 | 55 | + (UIImage*)imageByApplyingLightEffectToImage:(UIImage*)inputImage; 56 | + (UIImage*)imageByApplyingExtraLightEffectToImage:(UIImage*)inputImage; 57 | + (UIImage*)imageByApplyingDarkEffectToImage:(UIImage*)inputImage; 58 | + (UIImage*)imageByApplyingTintEffectWithColor:(UIColor *)tintColor toImage:(UIImage*)inputImage; 59 | 60 | //| ---------------------------------------------------------------------------- 61 | //! Applies a blur, tint color, and saturation adjustment to @a inputImage, 62 | //! optionally within the area specified by @a maskImage. 63 | //! 64 | //! @param inputImage 65 | //! The source image. A modified copy of this image will be returned. 66 | //! @param blurRadius 67 | //! The radius of the blur in points. 68 | //! @param tintColor 69 | //! An optional UIColor object that is uniformly blended with the 70 | //! result of the blur and saturation operations. The alpha channel 71 | //! of this color determines how strong the tint is. 72 | //! @param saturationDeltaFactor 73 | //! A value of 1.0 produces no change in the resulting image. Values 74 | //! less than 1.0 will desaturation the resulting image while values 75 | //! greater than 1.0 will have the opposite effect. 76 | //! @param maskImage 77 | //! If specified, @a inputImage is only modified in the area(s) defined 78 | //! by this mask. This must be an image mask or it must meet the 79 | //! requirements of the mask parameter of CGContextClipToMask. 80 | + (UIImage*)imageByApplyingBlurToImage:(UIImage*)inputImage withRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage; 81 | 82 | @end 83 | 84 | -------------------------------------------------------------------------------- /ImagePicker/UIImageEffects.m: -------------------------------------------------------------------------------- 1 | /* 2 | File: UIImageEffects.m 3 | Abstract: This class contains methods to apply blur and tint effects to an image. 4 | This is the code you’ll want to look out to find out how to use vImage to 5 | efficiently calculate a blur. 6 | Version: 1.1 7 | 8 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 9 | Inc. ("Apple") in consideration of your agreement to the following 10 | terms, and your use, installation, modification or redistribution of 11 | this Apple software constitutes acceptance of these terms. If you do 12 | not agree with these terms, please do not use, install, modify or 13 | redistribute this Apple software. 14 | 15 | In consideration of your agreement to abide by the following terms, and 16 | subject to these terms, Apple grants you a personal, non-exclusive 17 | license, under Apple's copyrights in this original Apple software (the 18 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 19 | Software, with or without modifications, in source and/or binary forms; 20 | provided that if you redistribute the Apple Software in its entirety and 21 | without modifications, you must retain this notice and the following 22 | text and disclaimers in all such redistributions of the Apple Software. 23 | Neither the name, trademarks, service marks or logos of Apple Inc. may 24 | be used to endorse or promote products derived from the Apple Software 25 | without specific prior written permission from Apple. Except as 26 | expressly stated in this notice, no other rights or licenses, express or 27 | implied, are granted by Apple herein, including but not limited to any 28 | patent rights that may be infringed by your derivative works or by other 29 | works in which the Apple Software may be incorporated. 30 | 31 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 32 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 33 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 34 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 35 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 36 | 37 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 38 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 39 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 40 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 41 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 42 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 43 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 44 | POSSIBILITY OF SUCH DAMAGE. 45 | 46 | Copyright (C) 2014 Apple Inc. All Rights Reserved. 47 | 48 | */ 49 | 50 | #import "UIImageEffects.h" 51 | 52 | @import Accelerate; 53 | 54 | @implementation UIImageEffects 55 | 56 | #pragma mark - 57 | #pragma mark - Effects 58 | 59 | //| ---------------------------------------------------------------------------- 60 | + (UIImage *)imageByApplyingLightEffectToImage:(UIImage*)inputImage 61 | { 62 | UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3]; 63 | return [self imageByApplyingBlurToImage:inputImage withRadius:60 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; 64 | } 65 | 66 | 67 | //| ---------------------------------------------------------------------------- 68 | + (UIImage *)imageByApplyingExtraLightEffectToImage:(UIImage*)inputImage 69 | { 70 | UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82]; 71 | return [self imageByApplyingBlurToImage:inputImage withRadius:40 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; 72 | } 73 | 74 | 75 | //| ---------------------------------------------------------------------------- 76 | + (UIImage *)imageByApplyingDarkEffectToImage:(UIImage*)inputImage 77 | { 78 | UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.73]; 79 | return [self imageByApplyingBlurToImage:inputImage withRadius:40 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; 80 | } 81 | 82 | 83 | //| ---------------------------------------------------------------------------- 84 | + (UIImage *)imageByApplyingTintEffectWithColor:(UIColor *)tintColor toImage:(UIImage*)inputImage 85 | { 86 | const CGFloat EffectColorAlpha = 0.6; 87 | UIColor *effectColor = tintColor; 88 | size_t componentCount = CGColorGetNumberOfComponents(tintColor.CGColor); 89 | if (componentCount == 2) { 90 | CGFloat b; 91 | if ([tintColor getWhite:&b alpha:NULL]) { 92 | effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha]; 93 | } 94 | } 95 | else { 96 | CGFloat r, g, b; 97 | if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) { 98 | effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha]; 99 | } 100 | } 101 | return [self imageByApplyingBlurToImage:inputImage withRadius:20 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil]; 102 | } 103 | 104 | #pragma mark - 105 | #pragma mark - Implementation 106 | 107 | //| ---------------------------------------------------------------------------- 108 | + (UIImage*)imageByApplyingBlurToImage:(UIImage*)inputImage withRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage 109 | { 110 | #define ENABLE_BLUR 1 111 | #define ENABLE_SATURATION_ADJUSTMENT 1 112 | #define ENABLE_TINT 1 113 | 114 | // Check pre-conditions. 115 | if (inputImage.size.width < 1 || inputImage.size.height < 1) 116 | { 117 | NSLog(@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", inputImage.size.width, inputImage.size.height, inputImage); 118 | return nil; 119 | } 120 | if (!inputImage.CGImage) 121 | { 122 | NSLog(@"*** error: inputImage must be backed by a CGImage: %@", inputImage); 123 | return nil; 124 | } 125 | if (maskImage && !maskImage.CGImage) 126 | { 127 | NSLog(@"*** error: effectMaskImage must be backed by a CGImage: %@", maskImage); 128 | return nil; 129 | } 130 | 131 | BOOL hasBlur = blurRadius > __FLT_EPSILON__; 132 | BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__; 133 | 134 | CGImageRef inputCGImage = inputImage.CGImage; 135 | CGFloat inputImageScale = inputImage.scale; 136 | CGBitmapInfo inputImageBitmapInfo = CGImageGetBitmapInfo(inputCGImage); 137 | CGImageAlphaInfo inputImageAlphaInfo = (inputImageBitmapInfo & kCGBitmapAlphaInfoMask); 138 | 139 | CGSize outputImageSizeInPoints = inputImage.size; 140 | CGRect outputImageRectInPoints = { CGPointZero, outputImageSizeInPoints }; 141 | 142 | // Set up output context. 143 | BOOL useOpaqueContext; 144 | if (inputImageAlphaInfo == kCGImageAlphaNone || inputImageAlphaInfo == kCGImageAlphaNoneSkipLast || inputImageAlphaInfo == kCGImageAlphaNoneSkipFirst) 145 | useOpaqueContext = YES; 146 | else 147 | useOpaqueContext = NO; 148 | UIGraphicsBeginImageContextWithOptions(outputImageRectInPoints.size, useOpaqueContext, inputImageScale); 149 | CGContextRef outputContext = UIGraphicsGetCurrentContext(); 150 | CGContextScaleCTM(outputContext, 1.0, -1.0); 151 | CGContextTranslateCTM(outputContext, 0, -outputImageRectInPoints.size.height); 152 | 153 | if (hasBlur || hasSaturationChange) 154 | { 155 | vImage_Buffer effectInBuffer; 156 | vImage_Buffer scratchBuffer1; 157 | 158 | vImage_Buffer *inputBuffer; 159 | vImage_Buffer *outputBuffer; 160 | 161 | vImage_CGImageFormat format = { 162 | .bitsPerComponent = 8, 163 | .bitsPerPixel = 32, 164 | .colorSpace = NULL, 165 | // (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little) 166 | // requests a BGRA buffer. 167 | .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, 168 | .version = 0, 169 | .decode = NULL, 170 | .renderingIntent = kCGRenderingIntentDefault 171 | }; 172 | 173 | vImage_Error e = vImageBuffer_InitWithCGImage(&effectInBuffer, &format, NULL, inputImage.CGImage, kvImagePrintDiagnosticsToConsole); 174 | if (e != kvImageNoError) 175 | { 176 | NSLog(@"*** error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", e, inputImage); 177 | UIGraphicsEndImageContext(); 178 | return nil; 179 | } 180 | 181 | vImageBuffer_Init(&scratchBuffer1, effectInBuffer.height, effectInBuffer.width, format.bitsPerPixel, kvImageNoFlags); 182 | inputBuffer = &effectInBuffer; 183 | outputBuffer = &scratchBuffer1; 184 | 185 | #if ENABLE_BLUR 186 | if (hasBlur) 187 | { 188 | // A description of how to compute the box kernel width from the Gaussian 189 | // radius (aka standard deviation) appears in the SVG spec: 190 | // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement 191 | // 192 | // For larger values of 's' (s >= 2.0), an approximation can be used: Three 193 | // successive box-blurs build a piece-wise quadratic convolution kernel, which 194 | // approximates the Gaussian kernel to within roughly 3%. 195 | // 196 | // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) 197 | // 198 | // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. 199 | // 200 | CGFloat inputRadius = blurRadius * inputImageScale; 201 | if (inputRadius - 2. < __FLT_EPSILON__) 202 | inputRadius = 2.; 203 | uint32_t radius = floor((inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5) / 2); 204 | 205 | radius |= 1; // force radius to be odd so that the three box-blur methodology works. 206 | 207 | NSInteger tempBufferSize = vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, NULL, 0, 0, radius, radius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend); 208 | void *tempBuffer = malloc(tempBufferSize); 209 | 210 | vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, radius, radius, NULL, kvImageEdgeExtend); 211 | vImageBoxConvolve_ARGB8888(outputBuffer, inputBuffer, tempBuffer, 0, 0, radius, radius, NULL, kvImageEdgeExtend); 212 | vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, radius, radius, NULL, kvImageEdgeExtend); 213 | 214 | free(tempBuffer); 215 | 216 | vImage_Buffer *temp = inputBuffer; 217 | inputBuffer = outputBuffer; 218 | outputBuffer = temp; 219 | } 220 | #endif 221 | 222 | #if ENABLE_SATURATION_ADJUSTMENT 223 | if (hasSaturationChange) 224 | { 225 | CGFloat s = saturationDeltaFactor; 226 | // These values appear in the W3C Filter Effects spec: 227 | // https://dvcs.w3.org/hg/FXTF/raw-file/default/filters/index.html#grayscaleEquivalent 228 | // 229 | CGFloat floatingPointSaturationMatrix[] = { 230 | 0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0, 231 | 0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0, 232 | 0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0, 233 | 0, 0, 0, 1, 234 | }; 235 | const int32_t divisor = 256; 236 | NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]); 237 | int16_t saturationMatrix[matrixSize]; 238 | for (NSUInteger i = 0; i < matrixSize; ++i) { 239 | saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor); 240 | } 241 | vImageMatrixMultiply_ARGB8888(inputBuffer, outputBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); 242 | 243 | vImage_Buffer *temp = inputBuffer; 244 | inputBuffer = outputBuffer; 245 | outputBuffer = temp; 246 | } 247 | #endif 248 | 249 | CGImageRef effectCGImage; 250 | if ( (effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, &cleanupBuffer, NULL, kvImageNoAllocate, NULL)) == NULL ) { 251 | effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, NULL, NULL, kvImageNoFlags, NULL); 252 | free(inputBuffer->data); 253 | } 254 | if (maskImage) { 255 | // Only need to draw the base image if the effect image will be masked. 256 | CGContextDrawImage(outputContext, outputImageRectInPoints, inputCGImage); 257 | } 258 | 259 | // draw effect image 260 | CGContextSaveGState(outputContext); 261 | if (maskImage) 262 | CGContextClipToMask(outputContext, outputImageRectInPoints, maskImage.CGImage); 263 | CGContextDrawImage(outputContext, outputImageRectInPoints, effectCGImage); 264 | CGContextRestoreGState(outputContext); 265 | 266 | // Cleanup 267 | CGImageRelease(effectCGImage); 268 | free(outputBuffer->data); 269 | } 270 | else 271 | { 272 | // draw base image 273 | CGContextDrawImage(outputContext, outputImageRectInPoints, inputCGImage); 274 | } 275 | 276 | #if ENABLE_TINT 277 | // Add in color tint. 278 | if (tintColor) 279 | { 280 | CGContextSaveGState(outputContext); 281 | CGContextSetFillColorWithColor(outputContext, tintColor.CGColor); 282 | CGContextFillRect(outputContext, outputImageRectInPoints); 283 | CGContextRestoreGState(outputContext); 284 | } 285 | #endif 286 | 287 | // Output image is ready. 288 | UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext(); 289 | UIGraphicsEndImageContext(); 290 | 291 | return outputImage; 292 | #undef ENABLE_BLUR 293 | #undef ENABLE_SATURATION_ADJUSTMENT 294 | #undef ENABLE_TINT 295 | } 296 | 297 | 298 | //| ---------------------------------------------------------------------------- 299 | // Helper function to handle deferred cleanup of a buffer. 300 | // 301 | void cleanupBuffer(void *userData, void *buf_data) 302 | { free(buf_data); } 303 | 304 | @end 305 | 306 | -------------------------------------------------------------------------------- /ImagePicker/VideoCameraCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LivePhotoCameraCell.swift 3 | // ExampleApp 4 | // 5 | // Created by Peter Stajger on 25/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | //TODO: add a recording indicator (red dot with timer) 13 | class VideoCameraCell : CameraCollectionViewCell { 14 | 15 | @IBOutlet weak var recordLabel: RecordDurationLabel! 16 | @IBOutlet weak var recordButton: RecordVideoButton! 17 | @IBOutlet weak var flipButton: UIButton! 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | recordButton.isEnabled = false 22 | recordButton.alpha = 0.5 23 | } 24 | 25 | @IBAction func recordButtonTapped(_ sender: UIButton) { 26 | if sender.isSelected { 27 | stopVideoRecording() 28 | } 29 | else { 30 | startVideoRecording() 31 | } 32 | } 33 | 34 | @IBAction func flipButtonTapped(_ sender: UIButton) { 35 | flipCamera() 36 | } 37 | 38 | // MARK: Override Methods 39 | 40 | override func updateRecordingVideoStatus(isRecording: Bool, shouldAnimate: Bool) { 41 | 42 | //update button state 43 | recordButton.isSelected = isRecording 44 | 45 | //update duration label 46 | isRecording ? recordLabel.start() : recordLabel.stop() 47 | 48 | //update other buttons 49 | let updates: () -> Void = { 50 | self.flipButton.alpha = isRecording ? 0 : 1 51 | } 52 | 53 | shouldAnimate ? UIView.animate(withDuration: 0.25, animations: updates) : updates() 54 | } 55 | 56 | override func videoRecodingDidBecomeReady() { 57 | recordButton.isEnabled = true 58 | UIView.animate(withDuration: 0.25) { 59 | self.recordButton.alpha = 1.0 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /ImagePicker/VideoCameraCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ImagePicker/VideoCaptureDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCaptureDelegate.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 04/10/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | import Photos 11 | 12 | final class VideoCaptureDelegate: NSObject, AVCaptureFileOutputRecordingDelegate { 13 | 14 | deinit { 15 | log("deinit: \(String(describing: self))") 16 | } 17 | 18 | // MARK: Public Methods 19 | 20 | /// set this to false if you dont wish to save video to photo library 21 | var savesVideoToLibrary = true 22 | 23 | /// true if user manually requested to cancel recording (stop without saving) 24 | var isBeingCancelled = false 25 | 26 | /// if system interrupts recording due to various reasons (empty space, phone call, background, ...) 27 | var recordingWasInterrupted = false 28 | 29 | /// non nil if failed or interrupted, nil if cancelled 30 | private(set) var recordingError: Error? 31 | 32 | init(didStart: @escaping ()->(), didFinish: @escaping (VideoCaptureDelegate)->(), didFail: @escaping (VideoCaptureDelegate, Error)->()) { 33 | self.didStart = didStart 34 | self.didFinish = didFinish 35 | self.didFail = didFail 36 | 37 | if UIDevice.current.isMultitaskingSupported { 38 | /* 39 | Setup background task. 40 | This is needed because the `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` 41 | callback is not received until AVCam returns to the foreground unless you request background execution time. 42 | This also ensures that there will be time to write the file to the photo library when AVCam is backgrounded. 43 | To conclude this background execution, endBackgroundTask(_:) is called in 44 | `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` after the recorded file has been saved. 45 | */ 46 | self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) 47 | } 48 | } 49 | 50 | // MARK: Private Methods 51 | 52 | private var backgroundRecordingID: UIBackgroundTaskIdentifier? = nil 53 | private var didStart: ()->() 54 | private var didFinish: (VideoCaptureDelegate)->() 55 | private var didFail: (VideoCaptureDelegate, Error)->() 56 | 57 | private func cleanUp(deleteFile: Bool, saveToAssets: Bool, outputFileURL: URL) { 58 | 59 | func deleteFileIfNeeded() { 60 | 61 | guard deleteFile == true else { return } 62 | 63 | let path = outputFileURL.path 64 | if FileManager.default.fileExists(atPath: path) { 65 | do { 66 | try FileManager.default.removeItem(atPath: path) 67 | } 68 | catch let error { 69 | log("capture session: could not remove recording at url: \(outputFileURL)") 70 | log("capture session: error: \(error)") 71 | } 72 | } 73 | } 74 | 75 | if let currentBackgroundRecordingID = backgroundRecordingID { 76 | backgroundRecordingID = UIBackgroundTaskInvalid 77 | if currentBackgroundRecordingID != UIBackgroundTaskInvalid { 78 | UIApplication.shared.endBackgroundTask(currentBackgroundRecordingID) 79 | } 80 | } 81 | 82 | if saveToAssets { 83 | PHPhotoLibrary.requestAuthorization { status in 84 | if status == .authorized { 85 | PHPhotoLibrary.shared().performChanges({ 86 | let creationRequest = PHAssetCreationRequest.forAsset() 87 | let videoResourceOptions = PHAssetResourceCreationOptions() 88 | videoResourceOptions.shouldMoveFile = true 89 | creationRequest.addResource(with: .video, fileURL: outputFileURL, options: videoResourceOptions) 90 | }, completionHandler: { success, error in 91 | if let error = error { 92 | log("capture session: Error occurered while saving video to photo library: \(error)") 93 | deleteFileIfNeeded() 94 | } 95 | }) 96 | } 97 | else { 98 | deleteFileIfNeeded() 99 | } 100 | } 101 | } 102 | else { 103 | deleteFileIfNeeded() 104 | } 105 | } 106 | 107 | // MARK: AVCaptureFileOutputRecordingDelegate Methods 108 | 109 | func fileOutput(_ captureOutput: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { 110 | didStart() 111 | } 112 | 113 | func fileOutput(_ captureOutput: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { 114 | 115 | if let error = error { 116 | recordingError = error 117 | 118 | log("capture session: movie recording failed error: \(error)") 119 | 120 | //this can be true even if recording is stopped due to a reason (no disk space, ...) so the video can still be delivered. 121 | let successfullyFinished = (((error as NSError).userInfo[AVErrorRecordingSuccessfullyFinishedKey] as AnyObject).boolValue) ?? false 122 | 123 | if successfullyFinished { 124 | recordingWasInterrupted = true 125 | cleanUp(deleteFile: true, saveToAssets: savesVideoToLibrary, outputFileURL: outputFileURL) 126 | didFail(self, error) 127 | } 128 | else { 129 | cleanUp(deleteFile: true, saveToAssets: false, outputFileURL: outputFileURL) 130 | didFail(self, error) 131 | } 132 | } 133 | else if isBeingCancelled == true { 134 | cleanUp(deleteFile: true, saveToAssets: false, outputFileURL: outputFileURL) 135 | didFinish(self) 136 | } 137 | else { 138 | cleanUp(deleteFile: true, saveToAssets: savesVideoToLibrary, outputFileURL: outputFileURL) 139 | didFinish(self) 140 | } 141 | 142 | } 143 | 144 | 145 | } 146 | -------------------------------------------------------------------------------- /ImagePicker/VideoOuptutSampleBufferDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoDataOuptutSampleBufferDelegate.swift 3 | // ImagePicker 4 | // 5 | // Created by Peter Stajger on 21/09/2017. 6 | // Copyright © 2017 Inloop. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | /* 13 | NOTE: if video file output is provided, video data output is not working!!! there must be only 1 output at the same time 14 | */ 15 | 16 | final class VideoOutputSampleBufferDelegate : NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { 17 | 18 | deinit { 19 | log("deinit: \(String(describing: self))") 20 | } 21 | 22 | let processQueue = DispatchQueue(label: "eu.inloop.video-output-sample-buffer-delegate.queue") 23 | 24 | var latestImage: UIImage? { 25 | return latestSampleBuffer?.imageRepresentation 26 | } 27 | 28 | private var latestSampleBuffer: CMSampleBuffer? 29 | 30 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 31 | latestSampleBuffer = sampleBuffer 32 | } 33 | 34 | } 35 | 36 | extension CMSampleBuffer { 37 | 38 | static let context = CIContext(options: [kCIContextUseSoftwareRenderer: false]) 39 | 40 | /// 41 | /// Converts Sample Buffer to UIImage with backing CGImage. This conversion 42 | /// is expensive, use it lazily. 43 | /// 44 | fileprivate var imageRepresentation: UIImage? { 45 | 46 | guard let pixelBuffer = CMSampleBufferGetImageBuffer(self) else { 47 | return nil 48 | } 49 | 50 | let ciImage = CIImage(cvPixelBuffer: pixelBuffer) 51 | 52 | // downscale image 53 | let filter = CIFilter(name: "CILanczosScaleTransform")! 54 | filter.setValue(ciImage, forKey: "inputImage") 55 | filter.setValue(0.25, forKey: "inputScale") 56 | filter.setValue(1.0, forKey: "inputAspectRatio") 57 | let resizedCiImage = filter.value(forKey: "outputImage") as! CIImage 58 | 59 | // TODO: consider using CIFilter also for bluring and saturating 60 | 61 | // we need to convert CIImage to CGImage because we are using Apples blurring 62 | // functions (see UIImage+ImageEffects.h) and it requires UIImage with 63 | // backed CGImage. This conversion is very expensive, use it only 64 | // when you really need it 65 | 66 | if let cgImage = CMSampleBuffer.context.createCGImage(resizedCiImage, from: resizedCiImage.extent) { 67 | return UIImage(cgImage: cgImage) 68 | } 69 | 70 | return nil 71 | } 72 | } 73 | --------------------------------------------------------------------------------