SwiftUI 从相册中选取图片
SwiftUI没有提供类似 ImagePicker 这样的视图给我们直接使用。为了实现可以从相册中选取图片的功能,我们需要创建一个 ImagePicker 视图。
使用UIKit的方式创建ImagePicker,需要使用到UIViewController的功能,所以我们创建一个遵循UIViewControllerRepresentable 协议的结构体。
struct ImagePicker: UIViewControllerRepresentable {
}
任何遵循 UIViewControllerRepresentable 协议的结构体,都需要实现 makeUiViewController 和 updateUIViewController 两个方法。我们添加上。
另外由于我们是返回一个 UIKit 中的 UIImagePickerController 对象,所以我们需要对默认生成的方法做一点小小的改造。
struct ImagePicker: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
return picker
}
func updateUIViewController(_ uiViewController: UIViewPickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
使用 UIViewController 通常还需要有代理对象(用于代理一些回调),在传统的UIKit编程中,通常代理对象就是Controller对象本身,但是在SwiftUI中,由于使用的是结构体而非类来创建Controller,所以无法使用Controller本身作为代理对象。
在这里我们使用 内联类 来创建一个代理类,这样就可以设置代理,并通过代理来管理一些事件回调。
struct ImagePicker: UIViewControllerRepresentable {
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker // 存放ImagePicker的引用
init(_ parent: ImagePicker) {
self.parent = parent
}
}
// 有内联类的情况下,必须创建一个初始化内联类对象的方法,
// 系统会自动调用这个函数创建内联类的对象,并将其绑定到context(上下文)中。
func makeCoordinator() -> Coordinator {
Coordinator(self) // 当方法内只有一行时,默认将这一行的结果返回
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> {
let picker = UIImagePickerController()
picker.delegate = context.coordinator // 设置代理,从上下文中获取代理对象
return picker
}
// 省略部分代码
}
接下来便是设置图片选取的回调函数了。
在内联的代理类中,我们可以重写 imagePickerController 方法来设置图片选取回调。
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage? // 用于存放UIImage
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
// 省略部分代码
// 设置图片选取回调
func imagePickerController(_ picker: UIImagePickerController, didFinishPickerMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let uiImage = info[.originalImage] as? UIImage { // 从info中获取原始图片,并转换为UIImage类型
parent.image = uiImage // 将数据返回
}
}
}
// 省略部分代码
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> {
let picker = UIImagePickerController()
picker.delegate = context.coordinator // 设置代理,从上下文中获取代理对象
return picker
}
// 省略部分代码
}
通常我们使用图片选择器的情况都是在视图中通过点击某个按钮打开一个新视图,并且在新视图中选择图片,选择完毕之后我们都需要关闭选择视图,返回到原视图中。
基于这样的一个操作逻辑,我们需要图片选择完毕之后关闭选择视图。
所以我们要在我们的ImagePicker中获取到环境变量中的 presentation 变量。
这个变量可以用来控制嵌套视图的状态。我们需要使用 dismiss 方法关闭选择视图。
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode // 获取环境变量
// 省略部分代码
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
// 省略部分代码
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
// 省略部分代码
parent.presentationMode.wrappedValue.dismiss(); // 关闭嵌套视图。
}
}
// 省略部分代码
}
我们的图片选择视图就这样完成啦。怎么使用呢?我们回到ContentView,创建一个使用图片选择器的视图。
struct ContentView: View {
@State private var image: Image?
@State private var showingImagePicker = false
@State private var inputImage: UIImage?
var body: some View {
NavigationView {
ZStack {
Rectangle()
.fill(Color.secondary)
if image != nil {
image?
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
}
.padding([.horizontal, .bottom])
.navigationBarTitle("ImagePicker")
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
}
稍微解释一下,由于我们想要使用嵌套视图来使用ImagePicker,那么最好的嵌套视图就是 NavigationView ,所以我们在body中使用 NavigationView 来容纳我们的视图。
接着是定义未选择图片和选择图片之后的视图。
然后定义了三个变量,分别用来存放图片、选择视图是否显示以及用于存放 UIImage 的变量。
通过使用导航器的 sheet 子视图,我们在子视图中使用 ImagePicker。
值得注意的是,我们还需要定义一个 onDismiss 回调,即 loadImage 方法。
loadImage 方法的功能是将 UIImage 转换成 Image 。
实际的使用效果如下
完成。