├── .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 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------