-
Notifications
You must be signed in to change notification settings - Fork 24
/
ImagePickerController.swift
437 lines (356 loc) · 17 KB
/
ImagePickerController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//
// This source file is part of the carousell/pickle open source project
//
// Copyright © 2017 Carousell and the project authors
// Licensed under Apache License v2.0
//
// See https://github.com/carousell/pickle/blob/master/LICENSE for license information
// See https://github.com/carousell/pickle/graphs/contributors for the list of project authors
//
// swiftlint:disable file_length
import UIKit
import Photos
/// Carousell flavoured image picker with multiple photo selections.
@objc
open class ImagePickerController: UINavigationController {
// MARK: - Initialization
/// An Objective-C compatible initializer without appearance configuration.
///
/// - Parameter selectedAssets: Preselected image assets that will be highlighted.
@objc
public convenience init(selectedAssets: [PHAsset]) {
self.init(selectedAssets: selectedAssets, configuration: nil)
}
/// Returns a newly initialized image picker controller with appearance configuration.
///
/// - Parameters:
/// - selectedAssets: Preselected image assets that will be highlighted. Default is an empty array.
/// - configuration: Optional appearance configuration. Default is nil.
public convenience init(selectedAssets: [PHAsset] = [], configuration: ImagePickerConfigurable? = nil) {
self.init(selectedAssets: selectedAssets, configuration: configuration, cameraType: UIImagePickerController.self)
}
/// Returns a newly initialized image picker controller with a customized type of camera.
///
/// - Parameters:
/// - selectedAssets: Preselected image assets that will be highlighted.
/// - configuration: Optional appearance configuration.
/// - cameraType: A UIViewController type that conforms to CameraCompatible protocol.
public convenience init<T: UIViewController>(
selectedAssets: [PHAsset],
configuration: ImagePickerConfigurable?,
cameraType: T.Type) where T: CameraCompatible {
self.init(selectedAssets: selectedAssets, configuration: configuration, camera: cameraType.init)
}
/// Returns a newly initialized image picker controller with a closure for camera configuration.
///
/// - Parameters:
/// - selectedAssets: Preselected image assets that will be highlighted.
/// - configuration: Optional appearance configuration.
/// - camera: A closure that returns a UIViewController that conforms to CameraCompatible protocol.
public init<T: UIViewController>(
selectedAssets: [PHAsset],
configuration: ImagePickerConfigurable?,
camera initializer: @escaping () -> T) where T: CameraCompatible {
self.selectedAssets = selectedAssets
self.configuration = configuration
self.allowedSelections = configuration?.allowedSelections ?? .unlimited
super.init(nibName: nil, bundle: nil)
if let cancelBarButtonItem = configuration?.cancelBarButtonItem {
cancelBarButtonItem.target = self
cancelBarButtonItem.action = #selector(cancel(_:))
self.cancelBarButton = cancelBarButtonItem
}
if let doneBarButtonItem = configuration?.doneBarButtonItem {
doneBarButtonItem.target = self
doneBarButtonItem.action = #selector(done(_:))
self.doneBarButton = doneBarButtonItem
}
camera = { [weak self] in
let camera = initializer()
camera.sourceType = .camera
camera.delegate = self
return camera
}
}
/// Returns an object initialized from data in a given unarchiver.
///
/// - Parameters:
/// - coder: An unarchiver object.
public required init?(coder aDecoder: NSCoder) {
self.selectedAssets = []
self.configuration = nil
self.allowedSelections = .unlimited
super.init(coder: aDecoder)
}
// MARK: - Properties
/// The image picker's delegate object, which should conform to ImagePickerControllerDelegate.
open override weak var delegate: UINavigationControllerDelegate? {
didSet {
imagePickerDelegate = delegate as? ImagePickerControllerDelegate
}
}
/// A localized string that shows on the navigation bar.
open override var title: String? {
didSet {
albumButton.title = title
albumButton.isHidden = title?.isEmpty ?? true
}
}
/// A localized string that shows above the photos.
public var hint: NSAttributedString? {
didSet {
galleryViewController?.hint = hint
}
}
fileprivate var selectedAssets: [PHAsset]
fileprivate let configuration: ImagePickerConfigurable?
fileprivate let allowedSelections: ImagePickerSelection
fileprivate weak var imagePickerDelegate: ImagePickerControllerDelegate?
fileprivate lazy var slideUpPresentation: UIViewControllerTransitioningDelegate = SlideUpPresentation()
fileprivate var galleryViewController: PhotoGalleryViewController? {
didSet {
// Remove the reference to the album button from the previous view controller.
oldValue?.navigationItem.titleView = nil
guard let galleryViewController = galleryViewController else {
setViewControllers([], animated: false)
return
}
setViewControllers([galleryViewController], animated: false)
galleryViewController.hint = hint
galleryViewController.delegate = self
galleryViewController.navigationItem.setLeftBarButton(cancelBarButton, animated: true)
galleryViewController.navigationItem.titleView = albumButton
galleryViewController.navigationItem.setRightBarButton(doneBarButton, animated: true)
doneBarButton.isEnabled = !selectedAssets.isEmpty
}
}
fileprivate lazy var camera: () -> UIViewController = {
let camera = UIImagePickerController()
camera.sourceType = .camera
camera.delegate = self
return camera
}
fileprivate lazy var emptyViewController: UIViewController = {
let controller = UIViewController()
controller.view.backgroundColor = UIColor.white
return controller
}()
fileprivate lazy var systemPhotoLibraryController: UIViewController = {
let photoLibrary = UIImagePickerController()
photoLibrary.sourceType = .photoLibrary
photoLibrary.configure(with: self.configuration)
photoLibrary.delegate = self
return photoLibrary
}()
fileprivate lazy var albumButton: PhotoAlbumTitleButton = {
let button = self.configuration.map(PhotoAlbumTitleButton.init) ?? PhotoAlbumTitleButton()
button.addTarget(self, action: #selector(togglePhotoAlbums(_:)), for: .touchUpInside)
return button
}()
fileprivate lazy var cancelBarButton: UIBarButtonItem =
UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(_:)))
fileprivate lazy var doneBarButton: UIBarButtonItem = {
let barButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done(_:)))
barButton.isEnabled = false
return barButton
}()
fileprivate lazy var photoAlbums: PHFetchResult<PHAssetCollection> =
PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil)
fileprivate lazy var favorites: PHFetchResult<PHAssetCollection> =
PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: nil)
fileprivate lazy var cameraRoll: PHFetchResult<PHAssetCollection> =
PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
/// A closure to present system permission message of photo library when the status is denied or restricted.
fileprivate var showPermissionErrorIfNeeded: (() -> Void)?
// MARK: - UIViewController
open override func viewDidLoad() {
super.viewDidLoad()
configure(with: configuration)
setViewControllers([emptyViewController], animated: false)
handle(photoLibraryPermission: PHPhotoLibrary.authorizationStatus())
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
showPermissionErrorIfNeeded?()
}
}
extension ImagePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
// MARK: - UIImagePickerControllerDelegate
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {
defer {
picker.dismiss(animated: true, completion: nil)
}
guard let originalImage = info[UIImagePickerControllerOriginalImage] as? UIImage else {
return
}
// Instead of using UIImagePickerControllerEditedImage, crop the original image for higher resolution if UIImagePickerControllerCropRect is specified.
var croppedImage: UIImage? = nil
if let cropRect = info[UIImagePickerControllerCropRect] as? CGRect, let cgImage = originalImage.cgImage?.cropping(to: cropRect) {
croppedImage = UIImage(cgImage: cgImage, scale: originalImage.scale, orientation: originalImage.imageOrientation)
}
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: croppedImage ?? originalImage)
}, completionHandler: nil)
}
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
switch PHPhotoLibrary.authorizationStatus() {
case .denied, .restricted:
picker.dismiss(animated: false) { [weak self] in
self?.cancel(nil)
}
default:
picker.dismiss(animated: true, completion: nil)
}
}
}
// MARK: - PhotoAlbumsViewControllerDelegate
extension ImagePickerController: PhotoAlbumsViewControllerDelegate {
internal func photoAlbumsViewController(_ controller: PhotoAlbumsViewController, didSelectAlbum album: PHAssetCollection) {
title = album.localizedTitle
galleryViewController = PhotoGalleryViewController(album: album, configuration: configuration)
dismiss(animated: true, completion: nil)
UIView.animate(withDuration: SlideUpPresentation.animationDuration) {
self.albumButton.isSelected = false
}
}
}
// MARK: - PhotoGalleryViewControllerDelegate
extension ImagePickerController: PhotoGalleryViewControllerDelegate {
internal func photoGalleryViewController(_ controller: PhotoGalleryViewController, shouldLaunchCameraWithAuthorization status: AVAuthorizationStatus) -> Bool {
return imagePickerDelegate?.imagePickerController(self, shouldLaunchCameraWithAuthorization: status) ?? true
}
internal func photoGalleryViewController(_ controller: PhotoGalleryViewController, shouldTogglePhoto asset: PHAsset) -> Bool {
if selectedAssets.index(of: asset) != nil {
return true
}
switch allowedSelections {
case .limit(to: let number) where 1 < number:
return selectedAssets.count < number
default:
return true
}
}
internal func photoGalleryViewController(_ controller: PhotoGalleryViewController, didTogglePhoto asset: PHAsset) {
if let selectedIndex = selectedAssets.index(of: asset) {
selectedAssets.remove(at: selectedIndex)
imagePickerDelegate?.imagePickerController?(self, didDeselectImageAsset: asset)
} else {
switch allowedSelections {
case .limit(to: let number) where 1 < number && selectedAssets.count < number:
fallthrough // swiftlint:disable:this fallthrough
case .unlimited:
selectedAssets.append(asset)
imagePickerDelegate?.imagePickerController?(self, didSelectImageAsset: asset)
case .limit(to: let number) where number == 1:
// When selecting only 1 photo, replace the selected one on every tap.
selectedAssets = [asset]
imagePickerDelegate?.imagePickerController?(self, didSelectImageAsset: asset)
default:
break
}
}
doneBarButton.isEnabled = !selectedAssets.isEmpty
}
internal func photoGalleryViewController(_ controller: PhotoGalleryViewController, taggedTextForPhoto asset: PHAsset) -> String? {
guard let index = selectedAssets.index(of: asset) else {
return nil
}
switch allowedSelections {
case .limit(to: let number) where number == 1:
return "✔︎"
default:
return String(index + 1)
}
}
internal func photoGalleryViewControllerDidSelectCameraButton(_ controller: PhotoGalleryViewController) {
launchCamera()
}
}
// MARK: - Private
fileprivate extension ImagePickerController {
fileprivate func handle(photoLibraryPermission status: PHAuthorizationStatus) {
switch status {
case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
self.handle(photoLibraryPermission: status)
self.showPermissionErrorIfNeeded?()
}
}
case .authorized:
title = cameraRoll.firstObject?.localizedTitle
galleryViewController = PhotoGalleryViewController(album: cameraRoll.firstObject, configuration: configuration)
case .denied, .restricted:
// Workaround the issue in iOS 11 where UIImagePickerController doesn't show the permission denied message.
// It requires additional PHAuthorizationStatus check before presenting Pickle.ImagePickerController.
if #available(iOS 11.0, *) {
// Hide the album button and display an empty gallery with a cancel button to dismiss the image picker.
title = nil
galleryViewController = PhotoGalleryViewController()
return
}
let controller = systemPhotoLibraryController
showPermissionErrorIfNeeded = { [weak self] in
self?.present(controller, animated: false, completion: {
self?.showPermissionErrorIfNeeded = nil
})
}
}
}
fileprivate func launchCamera() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
guard imagePickerDelegate?.imagePickerController(self, shouldLaunchCameraWithAuthorization: status) ?? true else {
return
}
switch status {
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { _ in
DispatchQueue.main.async {
self.launchCamera()
}
}
default:
present(camera(), animated: true, completion: nil)
}
}
private func transitioningDelegate(for controller: PhotoAlbumsViewController) -> UIViewControllerTransitioningDelegate {
if let transitioningDelegate = imagePickerDelegate?.imagePickerController?(self, transitioningDelegateForPresentingAlbumsViewController: controller) {
slideUpPresentation = transitioningDelegate
}
return slideUpPresentation
}
// MARK: IBActions
@objc
fileprivate func togglePhotoAlbums(_ sender: UIControl) {
let showsPhotoAlbums = !sender.isSelected
if showsPhotoAlbums {
let albums = imagePickerDelegate?.photoAlbumsForImagePickerController?(self) ?? [cameraRoll, favorites, photoAlbums]
let controller = PhotoAlbumsViewController(source: albums, configuration: configuration)
controller.delegate = self
controller.title = title
controller.modalPresentationStyle = .custom
controller.transitioningDelegate = transitioningDelegate(for: controller)
present(controller, animated: true, completion: nil)
} else {
presentedViewController?.dismiss(animated: true, completion: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// Remove the animation on the navigation bar added by system during modal presentation.
self.navigationBar.layer.removeAllAnimations()
UIView.animate(withDuration: SlideUpPresentation.animationDuration) {
sender.isSelected = showsPhotoAlbums
}
self.galleryViewController?.navigationItem.setLeftBarButton(showsPhotoAlbums ? nil : self.cancelBarButton, animated: true)
self.galleryViewController?.navigationItem.setRightBarButton(showsPhotoAlbums ? nil : self.doneBarButton, animated: true)
}
}
@objc
fileprivate func cancel(_ sender: UIBarButtonItem?) {
imagePickerDelegate?.imagePickerControllerDidCancel(self)
}
@objc
fileprivate func done(_ sender: UIBarButtonItem) {
imagePickerDelegate?.imagePickerController(self, didFinishPickingImageAssets: selectedAssets)
}
}
// swiftlint:enable