Categories
ImageIO iOS Swift SwiftUI UIKit

Using an image picker in SwiftUI

Lots of apps need to deal with selecting or taking photos but in SwiftUI we’ll need to wrap UIKit’s UIImagePickerController with a SwiftUI view.

Example application presenting a UI for opening image picker.

Wrapping UIImagePickerController in SwiftUI

UIImagePickerController has been available since iOS 2 and it supports both selecting photos from photo albums and taking new photos with a camera. If we would like to use an image picker in a SwiftUI view then the first step is wrapping this view controller with a SwiftUI view. UIViewControllerRepresentable protocol defines required methods for representing an UIViewController. We’ll provide a completion handler for passing back the selected image. We need to implement a coordinator which acts as a delegate for the UIImagePickerController. When the imagePickerController(_:didFinishPickingMediaWithInfo:) delegate method is called, then we can call the completion handler and handle the selected image in a SwiftUI view. As UIImagePickerController supports both the camera function and accessing existing photos, we’ll add a source type property for configuring which mode to use.

struct ImagePicker: UIViewControllerRepresentable {
typealias UIViewControllerType = UIImagePickerController
typealias SourceType = UIImagePickerController.SourceType
let sourceType: SourceType
let completionHandler: (UIImage?) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let viewController = UIImagePickerController()
viewController.delegate = context.coordinator
viewController.sourceType = sourceType
return viewController
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(completionHandler: completionHandler)
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let completionHandler: (UIImage?) -> Void
init(completionHandler: @escaping (UIImage?) -> Void) {
self.completionHandler = completionHandler
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
let image: UIImage? = {
if let image = info[.editedImage] as? UIImage {
return image
}
return info[.originalImage] as? UIImage
}()
completionHandler(image)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
completionHandler(nil)
}
}
}
ImagePicker view which wraps UIImagePickerController.

The ImagePicker can then be presented with the fullScreenCover view modifier. The presented state and the selected image is stored in the view’s view model. When the image picker is displayed and an image is selected, the completion handler is called and the selectedImage property is updated in the view model which in turn reloads the SwiftUI view.

var body: some View {
VStack(spacing: 32) {
imageView(for: viewModel.selectedImage)
controlBar()
}
.fullScreenCover(isPresented: $viewModel.isPresentingImagePicker, content: {
ImagePicker(sourceType: viewModel.sourceType, completionHandler: viewModel.didSelectImage)
})
}
Presenting the ImagePicker in full screen sheet.
extension ContentView {
final class ViewModel: ObservableObject {
@Published var selectedImage: UIImage?
@Published var isPresentingImagePicker = false
private(set) var sourceType: ImagePicker.SourceType = .camera
func choosePhoto() {
sourceType = .photoLibrary
isPresentingImagePicker = true
}
func takePhoto() {
sourceType = .camera
isPresentingImagePicker = true
}
func didSelectImage(_ image: UIImage?) {
selectedImage = image
isPresentingImagePicker = false
}
}
}
A SwiftUI view containing an image preview and buttons for taking or choosing a photo.

Summary

Wrapping UIKit views with a SwiftUI view is fairly simple. The coordinator object is a perfect fit for handling delegate methods which UIKit views often provide. As we saw, adding a SwiftUI compatible image picker was pretty easy to do. Please check the full example project on GitHub.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Project

SwiftUIImagePicker (Xcode 12.2)

Categories
iOS Swift

Taking photos on iOS

This time we will take a look on how to take photos and browse them on iOS.

Using UIImagePickerController

Creating user interface for taking photos consists of presenting an instance of UIImagePickerController. Controller needs a delegate and source type what defines if we are going to take a photo or pick a photo from the library. But even before creating the controller it is required to call isSourceTypeAvailable(_:) class method and verifying if the source type is available. For example, it returns false if picker’s sourceType is set to UIImagePickerController.SourceType.photoLibrary and the library is empty. Makes sense, as there are no photos to pick from.

@IBAction func takePhoto(_ sender: Any) {
openPhotoPicker(withSource: .camera)
}
@IBAction func choosePhoto(_ sender: Any) {
openPhotoPicker(withSource: .photoLibrary)
}
@discardableResult private func openPhotoPicker(withSource source: UIImagePickerController.SourceType) -> Bool {
guard UIImagePickerController.isSourceTypeAvailable(source) else { return false }
let picker: UIImagePickerController = {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = source
return picker
}()
present(picker, animated: true, completion:nil)
return true
}

UIImagePickerControllerDelegate contains two methods we need to implement for handling interactions in image picker. Firstly, consuming taken or picked images and secondly, dismissing the picker. Info dictionary in imagePickerController(_:didFinishPickingMediaWithInfo:) contains quite a many values from images to metadata. All the possible keys can be seen here. As a bare minimum we need to handle editedImage and originalImage keys. If it is required to add the image to photo library, then this can be done by calling UIImageWriteToSavedPhotosAlbum(). This starts an asynchronous operation for adding the image to library. And if needed, it is possible to specify callback when this operation finishes.

extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let handleImage: (UIImage?) -> () = { (image) in
self.imageView.image = image
self.imageView.isHidden = (image == nil)
if let image = image {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
if let image = info[.editedImage] as? UIImage {
handleImage(image)
}
else if let image = info[.originalImage] as? UIImage {
handleImage(image)
}
else {
handleImage(nil)
}
dismiss(animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
}

Privacy statements

Before going ahead and presenting the image picker it is required to add privacy statements to info.plist. Privacy - Camera Usage Description (NSCameraUsageDescription) is required for using the camera and Privacy - Photo Library Additions Usage Description (NSPhotoLibraryUsageDescription) when using UIImageWriteToSavedPhotosAlbum() for storing photos. Values of those keys are localised descriptions which are shown when app requests to use the camera or when adding the photo to the library.

Summary

Setting up user interface for taking photos is quite easy thanks to UIImagePickerController. All in all it was a three step process: configure picker, add privacy statements and handle images.

Sample app

PhotoTaker (GitHub) Xcode 10, Swift 4.2

References

UIImagePickerControllerDelegate (Apple)
NSCameraUsageDescription
NSPhotoLibraryUsageDescription