How to open the ImagePicker in SwiftUI?

后端 未结 8 1283
生来不讨喜
生来不讨喜 2020-11-29 23:53

I need to open the ImagePicker 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 Swi

相关标签:
8条回答
  • 2020-11-30 00:23

    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
    
    0 讨论(0)
  • 2020-11-30 00:24

    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.

    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.
    0 讨论(0)
  • 2020-11-30 00:26

    Cleaned up version for Xcode 11.4 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()
            }
    
        }
    
    }
    
    0 讨论(0)
  • 2020-11-30 00:26

    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:

    0 讨论(0)
  • 2020-11-30 00:32

    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.

    0 讨论(0)
  • 2020-11-30 00:33

    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)
    }
    
    0 讨论(0)
提交回复
热议问题