【问题标题】:How to open the ImagePicker in SwiftUI?如何在 SwiftUI 中打开 ImagePicker?
【发布时间】:2019-10-24 05:35:50
【问题描述】:

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

我考虑过使用UIImagePickerController,但我不知道如何在 SwiftUI 中做到这一点。

【问题讨论】:

标签: ios swift uiimagepickercontroller swiftui


【解决方案1】:

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

    }

}

【讨论】:

  • 这可以确定文件类型是 .png 还是 ,jpeg... 以便稍后上传到类似 firebase 存储?
【解决方案2】:

iOS 14 Xcode 12 - Photo Picker 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()
                }
            }
        }
    }
}

然后在 View 中你可以做

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

【讨论】:

  • 注意:别忘了import PhotosUI
【解决方案3】:

您需要将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>) {

    }

}

这是一个简单的测试视图:

  • 选择器显示在工作表中
  • 所选图像显示时没有任何类型的动画,并替换了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 不是相机时在选择器上发现了一个错误。 您将无法向下拖动工作表 - 我还没有找到解决方案。

【讨论】:

  • 谢谢!但是NavigationBar有问题:imgur.com/a/TIeX1mN
  • @Shahar 正如我所说,它的边缘很粗糙 - 我认为我们不应该提供这样的图像选择器。 Apple 最终将发布正确的 API 来执行此操作。此外,`UIImagePickerController` 是一个导航控制器,因此您可能希望隐藏 SwiftUI 的,否则您将拥有两个导航控制器。
  • 这在 Xcode 11 beta 4 中不再有效:“无法分配给属性:'$isShown' 是不可变的。无法分配给属性:'$image' 是不可变的”
  • 在 XCode 11.2 上运行良好
  • UIImagePickerController 存在一个错误,在选择任何图像之前无法通过向下拖动来消除它,我在这里发布了一个问题:stackoverflow.com/questions/60485214/…
【解决方案4】:

我是这样实现的:

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

}

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

【讨论】:

    【解决方案5】:

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

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

    所以我使用了一个主题来传输图像,您可以简单地观察它并将新图像添加到某个存储中,或者如果您希望它的行为就像一个绑定,这也是一行代码。 (在您的观察中修改您的状态)

    然后我将首选项包装到 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
                    }
                }
        }
    }
    

    【讨论】:

      【解决方案6】:

      基于@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>) {
      
          }
      
      }
      

      默认行为:

      【讨论】:

      【解决方案7】:

      这是适用于 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
      

      【讨论】:

      • BindableObject 现在是 ObservableObject
      【解决方案8】:

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

      这将加载一个图像选择器模式并让您选择一张照片,然后它将更新来自父级的@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,所以如果有人有一些 cmets,请告诉我!很高兴了解更多信息。

      【讨论】:

      • 什么是 self.userData?它是一个可观察对象,但究竟是什么符合可观察对象才能使其工作?
      猜你喜欢
      • 2020-04-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-13
      • 1970-01-01
      • 2019-07-13
      • 2020-02-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多