How to open the ImagePicker in SwiftUI?

21,344

Solution 1

Cleaned up version for Xcode 12 available via SPM as Swift Package:

https://github.com/ralfebert/ImagePickerView

Source:

import SwiftUI

public 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()
        }

    }

}

Solution 2

You need to wrap UIImagePickerController in a struct implementing UIViewControllerRepresentable.

For more about UIViewControllerRepresentable, please check this amazing WWDC 2019 talk:

Integrating SwiftUI

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.

enter image description here

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.

Solution 3

Based on @user:2890168 I made a version that:

  • retrieves UIImage instead of Image
  • use .sheet to present the ImagePicker.
  • shows ActionSheet to help users to remove or change the image.

enter image description here

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:

enter image description here

Solution 4

iOS 14 Xcode 12 - Photo Picker SwiftUI with Reusable View with limits allowed

struct ImagePickerView: UIViewControllerRepresentable {
    
    @Binding var images: [UIImage]
    @Binding var showPicker: Bool
    var selectionLimit: Int
    
    func makeUIViewController(context: Context) -> some UIViewController {
        var config = PHPickerConfiguration()
        config.filter = .images
        config.selectionLimit = selectionLimit
        let picker = PHPickerViewController(configuration: config)
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {

        var parent: ImagePickerView
        
        init(parent: ImagePickerView) {
            self.parent = parent
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            
            parent.showPicker.toggle()
            
            for img in results {
                if img.itemProvider.canLoadObject(ofClass: UIImage.self) {
                    img.itemProvider.loadObject(ofClass: UIImage.self) { (image, err) in
                        guard let image1 = image else { return }
                        
                        DispatchQueue.main.async {
                            self.parent.images.append(image1 as! UIImage)
                        }
                    }
                } else {
                    // Handle Error
                    parent.showPicker.toggle()
                }
            }
        }
    }
}

then in View you can do

VStack {          
    Image(systemName: "camera.viewfinder")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .onTapGesture {
            self.viewModel.pickerBool.toggle()
        }
}
.sheet(isPresented: self.$viewModel.pickerBool) {
    ImagePickerView(images: self.$viewModel.images, showPicker: self.$viewModel.pickerBool, selectionLimit: 3)
}

Solution 5

Here's a version that works in Xcode 11 beta 4.

It uses a BindableObject singleton (ImagePicker.shared) with two properties: .view and .image.

See usage below (ImagePickerTestView)

import SwiftUI
import Combine

final class ImagePicker : BindableObject {

    static let shared : ImagePicker = ImagePicker()

    private init() {}  //force using the singleton: ImagePicker.shared

    let view = ImagePicker.View()
    let coordinator = ImagePicker.Coordinator()

    // Bindable Object part
    let willChange = PassthroughSubject<Image?, Never>()

    @Published var image: Image? = nil {
        didSet {
            if image != nil {
                willChange.send(image)
            }
        }
    }
}


extension ImagePicker {

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

        // UIImagePickerControllerDelegate
        func imagePickerController(_ picker: UIImagePickerController,
                                   didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
            ImagePicker.shared.image = Image(uiImage: uiImage)
            picker.dismiss(animated:true)
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            picker.dismiss(animated:true)
        }
    }


    struct View: UIViewControllerRepresentable {

        func makeCoordinator() -> Coordinator {
            ImagePicker.shared.coordinator
        }

        func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker.View>) -> UIImagePickerController {
            let picker = UIImagePickerController()
            picker.delegate = context.coordinator
            return picker
        }

        func updateUIViewController(_ uiViewController: UIImagePickerController,
                                    context: UIViewControllerRepresentableContext<ImagePicker.View>) {

        }

    }

}


struct ImagePickerTestView: View {

    @State var showingPicker = false

    @State var image : Image? = nil
    // you could use ImagePicker.shared.image directly

    var body: some View {
        VStack {
            Button("Show image picker") {
                self.showingPicker = true
            }

            image?
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 300)

        }.sheet(isPresented: $showingPicker,
                onDismiss: {
                    // do whatever you need here
                }, content: {
                    ImagePicker.shared.view
                })
        .onReceive(ImagePicker.shared.$image) { image in
            // This gets called when the image is picked.
            // sheet/onDismiss gets called when the picker completely leaves the screen
            self.image = image
        }
    }

}

#if DEBUG
struct ImagePicker_Previews : PreviewProvider {

    static var previews: some View {
        ImagePickerTestView()
    }
}
#endif
Share:
21,344
Shahar Melamed
Author by

Shahar Melamed

Updated on August 03, 2022

Comments

  • Shahar Melamed
    Shahar Melamed almost 2 years

    I need to open an image picker in my app using SwiftUI, how can I do that?

    I thought about using the UIImagePickerController, but I don't know how to do that in SwiftUI.

  • Shahar Melamed
    Shahar Melamed almost 5 years
    Thanks! But there is a problem with the NavigationBar: imgur.com/a/TIeX1mN
  • Matteo Pacini
    Matteo Pacini almost 5 years
    @Shahar as I said, it's rough around the edges - I don't think we're supposed to present an image picker like that. Apple will eventually release the right API to do this. Also, the` UIImagePickerController` is a navigation controller, so you might want to hide the SwiftUI one, otherwise you will have two navigation controllers.
  • Zain
    Zain almost 5 years
    This no longer works in Xcode 11 beta 4: "Cannot assign to property: '$isShown' is immutable. Cannot assign to property: '$image' is immutable"
  • Sebbo
    Sebbo almost 5 years
    To get it to work in beta 4 just do what they did here forums.developer.apple.com/thread/120034. That is change @binding to Binding<Type>. Then when you set the values use image.value = Image(uiImage: uiImage)
  • Lydon Ch
    Lydon Ch over 4 years
    BindableObject is now ObservableObject
  • Peter
    Peter over 4 years
    The UIImagePickerController appears to not use the whole sheet area. How to make sure the view controller adapts to the sheet?
  • Peter
    Peter over 4 years
    Just as sidenote: the UIImagePickerController documentation states that a popover should be used (which did not make a difference in my case, though).
  • atulkhatri
    atulkhatri over 4 years
    Working fine on XCode 11.2
  • Joshua Hart
    Joshua Hart over 4 years
    What is self.userData? It's an observable object but what exactly conforms to observable object that would make this work?
  • JAHelia
    JAHelia about 4 years
    there is a bug with UIImagePickerController where it can't be dismissed by dragging down before choosing any image, I've posted a question for that here: stackoverflow.com/questions/60485214/…
  • Matteo Pacini
    Matteo Pacini about 4 years
    @JAHelia Indeed, I haven't found a workaround yet - you can only drag down the sheet if you're selecting the camera as the source.
  • Learn2Code
    Learn2Code about 4 years
    Might be a stupid question, but if user wanted to remove/delete the photo they just took with camera or selected from library, how would they go about that?
  • Learn2Code
    Learn2Code almost 4 years
    Can this determine if the file type is lets say .png vs ,jpeg... for later upload to like firebase storage?
  • Ozan Yasin Dogan
    Ozan Yasin Dogan about 3 years
    Note: Don't forget to import PhotosUI