Protocol Oriented approach to get Image or Video via UIImagePickerController on iOS
One typical use case in many mobile apps is selecting image or video with help of UIImagePickerController from image gallery or making a new one with the camera. Depending on the desired source and preferences, we need to configure certain UIImagePickerController’s properties and set the UIImagePickerControllerDelegate to the presenting UIViewController in order to be informed after the asset selection.
let imagePickerViewController = ImagePickerController()
imagePickerViewController.sourceType = .camera
imagePickerViewController.cameraDevice = .rear
imagePickerViewController.cameraCaptureMode = .photo
imagePickerViewController.showsCameraControls = true
imagePickerViewController.imagePickerDelegate = self
You can then access the selected asset in the delegate method as the following.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage
//Do some fancy stuff with your image}
The first time your app tries to access the photos or the camera, iOS will ask for access permissions with the description you provided inside your project plist fiel for the keys Privacy — Camera Usage Description, Privacy — Photo Library Usage Description.
This code above might get little bit longer and complex if you want to…
- ask your user the sourceType of the selection (camera, photoLibrary, savedPhotosAlbum)
- check for granted access permissions
- Alert and redirect users who have denied the access previously to privacy settings of your app.
In one of our applications, we needed to handle image selection in multiple UIViewControllers for different purposes and repeting the whole code for covering the mentioned cases would be betraying the DRY (Don’t Repeat Yourself). After some code review iterations, we ended up in a clean protocol oriented approach where all the logic is handled in a single place only.
We can define the following protocol and the default implementation so that any UIViewController can get the selected UIImage in the completion block.
Protocol
private var completionBlock: ((UIImage?) -> Void)?protocol ImagePickerPresenting: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func presentImagePicker(completion: @escaping (UIImage?) -> Void)
}
Default Implementation
extension ImagePickerPresenting where Self: UIViewController { func presentImagePicker(completion: @escaping (UIImage?) -> Void) {
completionBlock = completion
//Setup UIImageViewController }//MARK: UIImagePickerControllerDelegate func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
//Get the asset
completionBlock?(image)
completionBlock = nil
viewController.dismiss(animated: true, completion: nil)
} func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { }
}
That seems straightforward but the code will unfortunately not compile since the code above needs to be accessed by Objetvice-C but@objc
functions may not be in protocol extensions. As a simple walkaround we can wrap up the UIImagePickerController in a Swift class in the following way and let our protocol ImagePickerPresenting conform to the new wrapper swift class.
ImagePickerController
protocol ImagePickerControllerDelegate: class {
func imagePickerControllerDidFinish(image: UIImage?, _: ImagePickerController)
}final class ImagePickerController: UIImagePickerController {weak var imagePickerDelegate: ImagePickerControllerDelegate?override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}// MARK: — UIImagePickerControllerDelegateextension ImagePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])
let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage
imagePickerDelegate?.imagePickerControllerDidFinish(image: pickedImage, self)}func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
imagePickerDelegate?.imagePickerControllerDidFinish(image: nil, self)
}
}
Our ImagePickerPresenting protocol and the default implemantion looks like this after the changes.
Protocol
private var completionBlock: ((UIImage?) -> Void)?protocol ImagePickerPresenting: ImagePickerControllerDelegate {
func presentImagePicker(completion: @escaping (UIImage?) -> Void)
}
Default Implementation
extension ImagePickerPresenting where Self: UIViewController {
func presentImagePicker(completion: @escaping (UIImage?) -> Void) {
completionBlock = completion
let imagePickerViewController = ImagePickerController().
imagePickerViewController.imagePickerDelegate = self
//Check for device capabilities
//Show an ActionSheet to select desired source
//Check access rights to Photos/Camera
//If user has declined the access rights promt an alert to open app settingsself.present(imagePickerViewController, animated: true, completion: nil)
}}func imagePickerControllerDidFinish(image: UIImage?, _ viewController: ImagePickerController) {
completionBlock?(image)
completionBlock = nil
//completionBlock is a static variable, we need to set it to nil after calling the closure so that it does not live in memory. viewController.dismiss(animated: true, completion: nil)
}
Now we can simply call presentImagePicker function in any UIViewController and the selected image is passed as parameter to the closure. Instead of having multiple lines to setup UIImageViewController and implementing its delagete method, it’s as simple as one line of code. Apart from that it can be used any iOS project in that user needs to select an image from the Photos or camera.
presentImagePicker { (image) in
self.imageView.image = image
}
I wrapped up everthing in the sample project including the check for device capabilities and access right management.