How to show image picker in SwiftUI

Khoa Pham
Khoa Pham
Nov 6, 2019 · 3 min read

The easiest way to show image picker in iOS is to use UIImagePickerController, and we can bridge that to SwiftUI via UIViewControllerRepresentable

First attempt, use Environment

We conform to UIViewControllerRepresentable and make a Coordinator, which is the recommended way to manage the bridging with UIViewController.

There’s some built in environment property we can use, one of those is presentationMode where we can call dismiss to dismiss the modal.

My first attempt looks like below

import SwiftUI
import UIKit
public struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) private var presentationMode
@Binding var image: UIImage?
public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
presentationMode: presentationMode,
image: $image
)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
public func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
// No op
}
}
public extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate {
@Binding var presentationMode: PresentationMode
@Binding var image: UIImage?
public init(presentationMode: Binding<PresentationMode>, image: Binding<UIImage?>) {
self._presentationMode = presentationMode
self._image = image
}
}
}
extension ImagePicker.Coordinator: UIImagePickerControllerDelegate {
public func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
self.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
presentationMode.dismiss()
}
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}

Signatures

We need to be aware of the types of these property wrappers

Where we declare environment, presentationMode is of type Binding<PresentationMode>

@Environment(\.presentationMode) private var presentationMode

Given a Binding declaration, for example @Binding var image: UIImage?, image is of type UIImage? but $image is Binding<UIImage?>

public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
image: $image,
isPresented: $isPresented
)
}

When we want to assign to variables in init, we use _image to use mutable Binding<UIImage?> because self.$image gives us immutable Binding<UIImage?>

class Coordinator: NSObject, UINavigationControllerDelegate {
@Binding var presentationMode: PresentationMode
@Binding var image: UIImage?
public init(presentationMode: Binding<PresentationMode>, image: Binding<UIImage?>) {
self._presentationMode = presentationMode
self._image = image
}
}

How to use

To show modal, we use sheet and use a state @State var showImagePicker: Bool = false to control its presentation

Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$image)
})

Environment outside body

If we run the above code, it will crash because of we access environment value presentationMode in makeCoordinator and this is outside body

Fatal error: Reading Environment<Binding> outside View.body

public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
presentationMode: presentationMode,
image: $image
)
}

Second attempt, pass closure

So instead of passing environment presentationMode, we can pass closure, just like in React where we pass functions to child component.

So ImagePicker can just accept a closure called onDone, and the component that uses it can do the dismissal.

Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$image, onDone: {
self.presentationMode.wrappedValue.dismiss()
})
})

Unfortunately, although the onDone gets called, the modal is not dismissed.

Use Binding instead of Environment

Maybe there are betters way, but we can use Binding to replace usage of Environment.

We can do that by accepting Binding and change the isPresented state

import SwiftUI
import UIKit
public struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Binding var isPresented: Bool
public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
image: $image,
isPresented: $isPresented
)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
public func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
// No op
}
}
public extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate {
@Binding var isPresented: Bool
@Binding var image: UIImage?
public init(image: Binding<UIImage?>, isPresented: Binding<Bool>) {
self._image = image
self._isPresented = isPresented
}
}
}
extension ImagePicker.Coordinator: UIImagePickerControllerDelegate {
public func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
self.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
isPresented = false
}
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isPresented = false
}
}

How to use it

Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$image, isPresented: self.$showImagePicker)
})

Fantageek

Simple apps that make sense

Khoa Pham

Written by

Khoa Pham

My apps https://onmyway133.github.io/

Fantageek

Fantageek

Simple apps that make sense

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade