Created
November 3, 2017 17:46
-
-
Save sharplet/560e7c9f337c87ef1d0dde823d74afb6 to your computer and use it in GitHub Desktop.
An improved wrapper for UIImagePickerController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Here is a usage example. Refer to ImagePicker.swift below for the implementation! | |
// 1. Easily configure the picker | |
let cameraPicker = ImagePicker(sourceType: .camera) | |
let cropPicker = ImagePicker(sourceType: .photoLibrary, allowsEditing: true) | |
// Automatically includes both kUTTypeImage and kUTTypeLivePhoto | |
let livePhotoPicker = ImagePicker(sourceType: .photoLibrary, mediaTypes: [.livePhotos]) | |
// 2. Use the picker | |
final class UploadViewController: UIViewController { | |
@IBAction func choosePhoto(_ sender: Any?) { | |
cameraPicker.pickImage(over: self, animated: true) { (result: ImagePickerResult?) in | |
// if cancelled, result will be `nil` | |
guard let photo = result?.image else { return } | |
uploadImage(photo) | |
} | |
// 3. cancel a picking task | |
// grab a reference to the picking task | |
let task = cameraPicker.pickImage(over: self, animated: true) { result in | |
// ... | |
} | |
// Note: You can actually safely call `pickImage()` as many times as you want, | |
// and the task will just be enqueued to run later. No need to manage the state | |
// of "am I currently picking?" | |
// maybe we don't need this anymore | |
task.cancel() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Photos | |
import UIKit | |
/// Allows `ImagePicker` operation queues to safely target the main queue. | |
private let pickerQueue = DispatchQueue(label: "com.thoughtbot.uploadr.picker", target: .main) | |
struct ImagePickerResult { | |
struct EditingInfo { | |
let image: UIImage | |
let crop: CGRect | |
init?(_ info: [String: Any]) { | |
guard let image = info[UIImagePickerControllerEditedImage] as! UIImage?, | |
let crop = info[UIImagePickerControllerCropRect] as! NSValue? | |
else { return nil } | |
self.image = image | |
self.crop = crop.cgRectValue | |
} | |
} | |
let image: UIImage | |
let edits: EditingInfo? | |
let location: URL | |
let asset: PHAsset? | |
fileprivate init(_ info: [String: Any]) { | |
image = info[UIImagePickerControllerOriginalImage] as! UIImage | |
edits = EditingInfo(info) | |
location = info[UIImagePickerControllerImageURL] as! URL | |
asset = info[UIImagePickerControllerPHAsset] as! PHAsset? | |
} | |
} | |
/// An image picker with a specific `UIImagePickerController` configuration. | |
/// Defines a serial operation queue for image picking tasks, allowing `pickImage()` | |
/// to be called safely from any thread, and ensuring that only one image picking | |
/// task is active at a time. | |
final class ImagePicker { | |
private let picker = UIImagePickerController() | |
private let operationQueue = OperationQueue() | |
init(sourceType: UIImagePickerControllerSourceType, mediaTypes: ImagePickerMediaTypes = .images, allowsEditing: Bool = false) { | |
precondition(UIImagePickerController.isSourceTypeAvailable(sourceType), "Unavailable source type '\(sourceType.caseDescription)'.") | |
precondition(!mediaTypes.isEmpty, "You must provide at least one media type.") | |
let availableMediaTypes = ImagePickerMediaTypes | |
.availableMediaTypes(for: sourceType) | |
.intersection(mediaTypes) | |
precondition(!availableMediaTypes.isEmpty, "Requested media types not available: \(mediaTypes).") | |
picker.allowsEditing = allowsEditing | |
picker.mediaTypes = mediaTypes.imagePickerMediaTypes | |
picker.sourceType = sourceType | |
operationQueue.maxConcurrentOperationCount = 1 | |
operationQueue.underlyingQueue = pickerQueue | |
} | |
/// Present a `UIImagePickerController` in a specific configuration, in a thread-safe way. | |
/// Multiple calls to this method will safely enqueue image picking tasks, removing the | |
/// need for the caller to manage state. The completion handler is called on the main queue. | |
@discardableResult | |
func pickImage(over context: UIViewController, animated: Bool, completionHandler: @escaping (ImagePickerResult?) -> Void) -> ImagePickerTask { | |
let operation = ImagePickerOperation(context: context, picker: picker, animated: animated) | |
operation.completionBlock = { [unowned operation] in | |
let result = operation.result | |
DispatchQueue.main.async { | |
completionHandler(result) | |
} | |
} | |
operationQueue.addOperation(operation) | |
return ImagePickerTask(operation) | |
} | |
/// Cancels all current and pending image picker operations. | |
func cancelAll() { | |
operationQueue.cancelAllOperations() | |
} | |
} | |
/// A handle to an image picker operation, allowing it to be cancelled. | |
final class ImagePickerTask { | |
private let operation: ImagePickerOperation | |
fileprivate init(_ operation: ImagePickerOperation) { | |
self.operation = operation | |
} | |
func cancel() { | |
operation.cancel() | |
} | |
} | |
/// Manages the state of a single image picking operation. | |
/// | |
/// - Requires: Must be started on `pickerQueue`. | |
private final class ImagePickerOperation: Operation, UIImagePickerControllerDelegate, UINavigationControllerDelegate { | |
let animated: Bool | |
let context: UIViewController | |
let picker: UIImagePickerController | |
private(set) var state: ImagePickingState | |
init(context: UIViewController, picker: UIImagePickerController, animated: Bool) { | |
self.animated = animated | |
self.context = context | |
self.picker = picker | |
self.state = .ready | |
super.init() | |
} | |
var result: ImagePickerResult? { | |
switch state { | |
case let .completed(result): | |
return result | |
case .ready, .executing, .cancelledByUser: | |
return nil | |
} | |
} | |
override var isAsynchronous: Bool { | |
return true | |
} | |
override var isExecuting: Bool { | |
return state.isExecuting | |
} | |
override var isFinished: Bool { | |
return state.isFinished | |
} | |
override func start() { | |
dispatchPrecondition(condition: .onQueue(pickerQueue)) | |
guard !isCancelled else { | |
willChangeValue(for: \.isFinished) | |
state = .completed(nil) | |
didChangeValue(for: \.isFinished) | |
return | |
} | |
picker.delegate = self | |
willChangeValue(for: \.isExecuting) | |
context.present(picker, animated: animated) { | |
self.state = .executing | |
self.didChangeValue(for: \.isExecuting) | |
} | |
} | |
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) { | |
willChangeValue(for: \.isExecuting) | |
willChangeValue(for: \.isFinished) | |
let result = isCancelled ? nil : ImagePickerResult(info) | |
state = .completed(result) | |
context.dismiss(animated: true) { | |
self.didChangeValue(for: \.isExecuting) | |
self.didChangeValue(for: \.isFinished) | |
} | |
} | |
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { | |
willChangeValue(for: \.isExecuting) | |
willChangeValue(for: \.isFinished) | |
state = .cancelledByUser | |
context.dismiss(animated: true) { | |
self.didChangeValue(for: \.isExecuting) | |
self.didChangeValue(for: \.isFinished) | |
} | |
} | |
} | |
private enum ImagePickingState { | |
case ready | |
case executing | |
case cancelledByUser | |
case completed(ImagePickerResult?) | |
var isExecuting: Bool { | |
switch self { | |
case .executing: | |
return true | |
case .ready, .cancelledByUser, .completed: | |
return false | |
} | |
} | |
var isFinished: Bool { | |
switch self { | |
case .cancelledByUser, .completed: | |
return true | |
case .ready, .executing: | |
return false | |
} | |
} | |
} | |
private extension UIImagePickerControllerSourceType { | |
@nonobjc var caseDescription: String { | |
let typeName = String(describing: type(of: self)) | |
let caseName: String | |
switch self { | |
case .camera: | |
caseName = "camera" | |
case .photoLibrary: | |
caseName = "photoLibrary" | |
case .savedPhotosAlbum: | |
caseName = "savedPhotosAlbum" | |
} | |
return "\(typeName).\(caseName)" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import MobileCoreServices | |
import UIKit | |
struct ImagePickerMediaTypes: OptionSet { | |
var rawValue: Set<CFString> | |
init(rawValue: Set<CFString>) { | |
self.rawValue = rawValue | |
} | |
var imagePickerMediaTypes: [String] { | |
return rawValue.map { $0 as String } | |
} | |
static let images = ImagePickerMediaTypes(rawValue: [kUTTypeImage]) | |
static let livePhotos = ImagePickerMediaTypes(rawValue: [kUTTypeImage, kUTTypeLivePhoto]) | |
static let movies = ImagePickerMediaTypes(rawValue: [kUTTypeMovie]) | |
static func availableMediaTypes(for sourceType: UIImagePickerControllerSourceType) -> ImagePickerMediaTypes { | |
let mediaTypes = UIImagePickerController.availableMediaTypes(for: sourceType) ?? [] | |
return ImagePickerMediaTypes(rawValue: Set(mediaTypes as [CFString])) | |
} | |
} | |
extension ImagePickerMediaTypes: SetAlgebra { | |
init() { | |
self.init(rawValue: []) | |
} | |
mutating func formUnion(_ other: ImagePickerMediaTypes) { | |
rawValue.formUnion(other.rawValue) | |
} | |
mutating func formIntersection(_ other: ImagePickerMediaTypes) { | |
rawValue.formIntersection(other.rawValue) | |
} | |
mutating func formSymmetricDifference(_ other: ImagePickerMediaTypes) { | |
rawValue.formSymmetricDifference(other.rawValue) | |
} | |
} | |
extension ImagePickerMediaTypes: CustomStringConvertible { | |
var description: String { | |
let names = rawValue.lazy.map { $0 as String }.joined(separator: ", ") | |
return "(\(names))" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment