Skip to content

Instantly share code, notes, and snippets.

@sharplet
Created November 3, 2017 17:46
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sharplet/560e7c9f337c87ef1d0dde823d74afb6 to your computer and use it in GitHub Desktop.
Save sharplet/560e7c9f337c87ef1d0dde823d74afb6 to your computer and use it in GitHub Desktop.
An improved wrapper for UIImagePickerController
// 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()
}
}
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)"
}
}
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