How to open the ImagePicker in SwiftUI?
You need to wrap UIImagePickerController
in a struct implementing UIViewControllerRepresentable
.
For more about UIViewControllerRepresentable
, please check this amazing WWDC 2019 talk:
struct ImagePicker: UIViewControllerRepresentable { @Environment(\.presentationMode) private var presentationMode let sourceType: UIImagePickerController.SourceType let onImagePicked: (UIImage) -> Void final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { @Binding private var presentationMode: PresentationMode private let sourceType: UIImagePickerController.SourceType private let onImagePicked: (UIImage) -> Void init(presentationMode: Binding<PresentationMode>, sourceType: UIImagePickerController.SourceType, onImagePicked: @escaping (UIImage) -> Void) { _presentationMode = presentationMode self.sourceType = sourceType self.onImagePicked = onImagePicked } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage onImagePicked(uiImage) presentationMode.dismiss() } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { presentationMode.dismiss() } } func makeCoordinator() -> Coordinator { return Coordinator(presentationMode: presentationMode, sourceType: sourceType, onImagePicked: onImagePicked) } func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = sourceType picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) { }}
Here's a simple view to test it:
- The picker is displayed in a sheet
- the selected image appears without any sort of animation, and replaces the
Show image picker
button
struct ContentView: View { @State var showImagePicker: Bool = false @State var image: Image? = nil var body: some View { ZStack { VStack { Button(action: { self.showImagePicker.toggle() }) { Text("Show image picker") } image?.resizable().frame(width: 100, height: 100) } .sheet(isPresented: $showImagePicker) { ImagePicker(sourceType: .photoLibrary) { image in self.image = Image(uiImage: image) } } } }}
I hope this helps as a starting point!
I'm sure Apple will make this easier to do once SwiftUI
is out of beta.
Tested on Xcode 11.4
Bugs:
- @JAHelia found a bug on the picker when
sourceType
is not the camera.You won't be able to drag down the sheet - I haven't been able to find a solution yet.
Based on @user:2890168 I made a version that:
- retrieves
UIImage
instead ofImage
- use
.sheet
to present theImagePicker
. - shows
ActionSheet
to help users to remove or change the image.
struct LibraryImage: View { @State var showAction: Bool = false @State var showImagePicker: Bool = false @State var uiImage: UIImage? = nil var sheet: ActionSheet { ActionSheet( title: Text("Action"), message: Text("Quotemark"), buttons: [ .default(Text("Change"), action: { self.showAction = false self.showImagePicker = true }), .cancel(Text("Close"), action: { self.showAction = false }), .destructive(Text("Remove"), action: { self.showAction = false self.uiImage = nil }) ]) } var body: some View { VStack { if (uiImage == nil) { Image(systemName: "camera.on.rectangle") .accentColor(Color.App.purple) .background( Color.App.gray .frame(width: 100, height: 100) .cornerRadius(6)) .onTapGesture { self.showImagePicker = true } } else { Image(uiImage: uiImage!) .resizable() .frame(width: 100, height: 100) .cornerRadius(6) .onTapGesture { self.showAction = true } } } .sheet(isPresented: $showImagePicker, onDismiss: { self.showImagePicker = false }, content: { ImagePicker(isShown: self.$showImagePicker, uiImage: self.$uiImage) }) .actionSheet(isPresented: $showAction) { sheet } }}
The default body
of LibraryImage
is an Image
that shows a camera icon that is tappable by the users.
On tap event, the image picker is shown with a sheet
modifier. After the image selection, the LibraryImage
body is recomputed and now shows the Image
defined in else statement (because uiImage
property now contains the image picked by the user).
Now, on tap event the ActionSheet
is shown.
The edited image picker:
struct ImagePicker: UIViewControllerRepresentable { @Binding var isShown: Bool @Binding var uiImage: UIImage? class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { @Binding var isShown: Bool @Binding var uiImage: UIImage? init(isShown: Binding<Bool>, uiImage: Binding<UIImage?>) { _isShown = isShown _uiImage = uiImage } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { let imagePicked = info[UIImagePickerController.InfoKey.originalImage] as! UIImage uiImage = imagePicked isShown = false } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { isShown = false } } func makeCoordinator() -> Coordinator { return Coordinator(isShown: $isShown, uiImage: $uiImage) } func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { let picker = UIImagePickerController() picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) { }}
default behaviour:
Cleaned up version for Xcode 12 available via SPM as Swift Package:
https://github.com/ralfebert/ImagePickerView
Source:
import SwiftUIpublic struct ImagePickerView: UIViewControllerRepresentable { private let sourceType: UIImagePickerController.SourceType private let onImagePicked: (UIImage) -> Void @Environment(\.presentationMode) private var presentationMode public init(sourceType: UIImagePickerController.SourceType, onImagePicked: @escaping (UIImage) -> Void) { self.sourceType = sourceType self.onImagePicked = onImagePicked } public func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = self.sourceType picker.delegate = context.coordinator return picker } public func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} public func makeCoordinator() -> Coordinator { Coordinator( onDismiss: { self.presentationMode.wrappedValue.dismiss() }, onImagePicked: self.onImagePicked ) } final public class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { private let onDismiss: () -> Void private let onImagePicked: (UIImage) -> Void init(onDismiss: @escaping () -> Void, onImagePicked: @escaping (UIImage) -> Void) { self.onDismiss = onDismiss self.onImagePicked = onImagePicked } public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let image = info[.originalImage] as? UIImage { self.onImagePicked(image) } self.onDismiss() } public func imagePickerControllerDidCancel(_: UIImagePickerController) { self.onDismiss() } }}