How to open the ImagePicker in SwiftUI?

为君一笑 提交于 2019-11-27 13:20:51

Here's a "rough around the edges" implementation.

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 {

    @Binding var isShown: Bool
    @Binding var image: Image?

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

        @Binding var isShown: Bool
        @Binding var image: Image?

        init(isShown: Binding<Bool>, image: Binding<Image?>) {
            _isShown = isShown
            _image = image
        }

        func imagePickerController(_ picker: UIImagePickerController,
                                   didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
            image = Image(uiImage: uiImage)
            isShown = false
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            isShown = false
        }

    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(isShown: $isShown, image: $image)
    }

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

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

    }

}

Here's a simple view to test it.

It's rough in the sense that:

  • the picker is displayed right on top of the view, without transition
  • 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: {
                    withAnimation {
                        self.showImagePicker.toggle()
                    }
                }) {
                    Text("Show image picker")
                }
                image?.resizable().frame(width: 100, height: 100)
            }
            if (showImagePicker) {
                ImagePicker(isShown: $showImagePicker, image: $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.

Updated for Xcode 11 beta 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

I'm very new at Swift, but I was able to get it with the following.

This will load up an image picker modal and let you select a photo, and it will then update an @State variable from a parent.

If this works for you, you can replace the @State with something that can span across multiple components, such as @EnvironmentObject so other components can get updated as well.

Hope this helps!

// ImagePicker.swift

struct ImagePicker : View {   
    @State var image: UIImage? = nil

    var body: some View {
        ImagePickerViewController(image: $image)
    }
}
// ImagePickerViewController.swift

import UIKit
import AVFoundation
import SwiftUI


struct ImagePickerViewController: UIViewControllerRepresentable {
    @Binding var image: UIImage?

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerViewController>) {
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePickerViewController>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = UIImagePickerController.SourceType.photoLibrary
        imagePicker.allowsEditing = false
        imagePicker.delegate = context.coordinator
        return imagePicker
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {

        var parent: ImagePickerViewController

        init(_ parent: ImagePickerViewController) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            let imagePicked = info[.originalImage] as! UIImage            
            parent.image = imagePicked
            picker.dismiss(animated: true, completion: nil)
        }
    }
}

Usage:

// SampleView.swift

struct SampleView : View {
    var body: some View {
        PresentationLink(destination: ImagePicker().environmentObject(self.userData), label: {
           Text("Import Photo")
       })
    }
}

Once again, I am fresh into Swift so if anyone has some comments, please let me know! Happy to learn more.

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.

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:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!