如何在 SwiftUI 中打开 ImagePicker?

How to open the ImagePicker in SwiftUI?

我需要使用 SwiftUI 在我的应用程序中打开一个图像选择器,我该怎么做?

我考虑过使用 UIImagePickerController,但我不知道如何在 SwiftUI 中使用它。

您需要将 UIImagePickerController 包装在实现 UIViewControllerRepresentable.

的结构中

有关 UIViewControllerRepresentable 的更多信息,请查看这个精彩的 WWDC 2019 演讲:

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>) {

    }

}

这里有一个简单的测试视图:

  • 选取器显示在 sheet
  • 所选图像出现时没有任何动画,并替换了 Show image picker 按钮
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)
                }
            }
        }
    }
}

我希望这有助于作为一个起点!

我相信一旦 SwiftUI 结束测试版,Apple 会让这件事变得更容易。

测试于 Xcode 11.4

错误:

  • @JAHelia 发现当 sourceType 不是相机时拾取器上的错误。 你将无法拖下 sheet - 我还没有找到解决方案。

我是 Swift 的新手,但我能够通过以下方式获得它。

这将加载一个图像选择器模态并让您 select 一张照片,然后它将更新来自父项的 @State 变量。

如果这对您有用,您可以将 @State 替换为可以跨多个组件的内容,例如 @EnvironmentObject,这样其他组件也可以得到更新。

希望对您有所帮助!

// 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)
        }
    }
}

用法:

// SampleView.swift

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

再一次,我刚接触 Swift 所以如果有人有任何意见,请告诉我!很高兴了解更多。

这是一个适用于 Xcode 11 beta 4 的版本。

它使用具有两个属性的 BindableObject 单例 (ImagePicker.shared):.view 和 .image。

请参阅下面的用法 (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

基于@user:2890168 我做了一个版本:

  • 检索 UIImage 而不是 Image
  • 使用.sheet来呈现ImagePicker
  • 显示 ActionSheet 以帮助用户删除或更改图像。

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
        }
    }
}

LibraryImage 的默认 body 是一个 Image,显示用户可点击的相机图标。

点击事件时,图像选择器显示为带有 sheet 修饰符。选择图像后,重新计算 LibraryImage 主体,现在显示在 else 语句中定义的 Image(因为 uiImage 属性 现在包含用户选择的图像)。

现在,点击事件显示 ActionSheet

编辑后的图片选择器:

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>) {

    }

}

默认行为:

我实现了一个我认为更通用和可扩展的版本。我使用 Subject 而不是 Binding 来解决 undoable/inappropriate 在您的视图中添加另一个绑定的问题。

例如,您创建了一个 List 显示存储在基础存储中的一组图像,并且您想要使用图像选择器添加图像。在这种情况下,非常 hard/ugly 将该图像添加到您的基础存储中。

所以我使用了一个主题来传输图像,你可以简单地观察它并将新图像添加到一些存储中,或者如果你想让它表现得像一个绑定,它也是一行代码。 (在你的观察中修改你的状态)

然后我将首选项包装到 ViewModel 中,这样如果您想要更多主题或配置,它就不会变得混乱。

import SwiftUI
import Combine

struct ImagePickerView : UIViewControllerRepresentable {

    @Binding var model: ImagePickerViewModel

    typealias UIViewControllerType = UIImagePickerController

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

    func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIImagePickerController {

        let controller = UIImagePickerController()
        controller.delegate = context.coordinator
        controller.allowsEditing = false
        controller.mediaTypes = ["public.image"]
        controller.sourceType = .photoLibrary
        return controller

    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerView>) {
        // run right after making

    }

    class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

        var parentView: ImagePickerView

        init(_ parentView: ImagePickerView) {
            self.parentView = parentView
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parentView.model.isPresented = false
        }

        func imagePickerController(_ picker: UIImagePickerController,
                                          didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {

            guard let uiImage = info[.originalImage] as? UIImage else { return }

            let image = Image(uiImage: uiImage)
            parentView.model.pickedImagesSubject?.send([image])
            parentView.model.isPresented = false

        }

    }

}

struct ImagePickerViewModel {
    var isPresented: Bool = false
    let pickedImagesSubject: PassthroughSubject<[Image], Never>! = PassthroughSubject<[Image], Never>()
}

用法:

struct SomeView : View {

    @EnvironmentObject var storage: Storage
    @State var imagePickerViewModel = ImagePickerViewModel()

    var body: some View {
        Button(action: { self.imagePickerViewModel.isPresented.toggle() }) { ... }
            .sheet(isPresented: $imagePickerViewModel.isPresented) {
                ImagePickerView(model: self.$imagePickerViewModel)
            }
            .onReceive(imagePickerViewModel.pickedImagesSubject) { (images: [Image]) -> Void in
                withAnimation {
                    // modify your storage here
                    self.storage.images += images
                }
            }
    }
}

我是这样实现的:

    import SwiftUI

    final class ImagePickerCoordinator: NSObject {

        @Binding var image: UIImage?
        @Binding var takePhoto: Bool

        init(image: Binding<UIImage?>, takePhoto: Binding<Bool>) {
            _image = image
            _takePhoto = takePhoto
        }
    }

    struct ShowImagePicker: UIViewControllerRepresentable {

        @Binding var image: UIImage?
        @Binding var takePhoto: Bool

        func makeCoordinator() -> ImagePickerCoordinator {
            ImagePickerCoordinator(image: $image, takePhoto: $takePhoto)
        }

        func makeUIViewController(context: Context) -> UIImagePickerController {

            let pickerController = UIImagePickerController()
            pickerController.delegate = context.coordinator

            guard UIImagePickerController.isSourceTypeAvailable(.camera) else { return pickerController }

            switch self.takePhoto {
            case true:
                pickerController.sourceType = .camera
            case false:
                pickerController.sourceType = .photoLibrary
            }

            pickerController.allowsEditing = true
            return pickerController
        }

        func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
    }

    extension ImagePickerCoordinator: UINavigationControllerDelegate, UIImagePickerControllerDelegate {

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

            guard let uiImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }

            self.image = uiImage
            picker.dismiss(animated: true, completion: nil)
        }

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

}

将仅两个按钮的逻辑添加到您的视图中就足够了...))

Xcode 12 的清理版本可通过 SPM 作为 Swift 包:

https://github.com/ralfebert/ImagePickerView

来源:

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

    }

}

iOS 14 Xcode 12 - 照片选择器 SwiftUI 具有可重复使用的视图,允许限制

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

然后在视图中你可以做

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