使用协调器管理SwiftUI视图控制器
之前,我们研究了如何使用UIViewControllerRepresentable
包装UIKit视图控制器,以便可以在SwiftUI中使用它,尤其是UIImagePickerController
。但是,我们遇到了一个问题:尽管我们可以显示图像选择器,但是当用户选择图像时我们没有收到通知。
SwiftUI对此的解决方案称为协调器(coordinators),这对于来自UIKit背景的人们来说有点令人困惑,因为在那里我们有一种设计模式,也称为协调器,其执行了完全不同的角色。需要明确的是,SwiftUI的协调器与许多UIKit开发人员使用的协调器模式完全不同,因此,如果您以前使用过这种模式,请从大脑中抛弃它,以免造成混淆!
SwiftUI的协调器旨在充当UIKit视图控制器的委托。请记住,“委托”是对其他地方发生的事件做出响应的对象。例如,UIKit使我们可以将委托对象附加到其文本字段视图,当用户键入任何内容,按回车键等等时,该委托将得到通知。这意味着UIKit开发人员可以修改其文本字段的行为方式,而不必创建自己的自定义文本字段类型。
在SwiftUI中使用协调器需要您了解UIKit的工作方式,这不足为奇,因为我们实际上是在整合UIKit的视图控制器。因此,为了演示这一点,我们将升级ImagePicker
视图,以便可以在选择图像时报告。
提醒一下,这是我们现在拥有的代码:
struct ImagePicker: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
我们将逐步进行,因为这里有很多事情要做——如果需要您花一些时间来理解,不需要感到难过,因为第一次遇到协调器确实不那么容易。
首先,在ImagePicker
结构体中添加此嵌套类:
class Coordinator {
}
是的,您需要马上上一堂课。它不必是嵌套类,尽管它是一个好主意,因为它巧妙地封装了功能——如果没有嵌套类,那么如果您将许多视图控制器和协调器混在一起,将会造成混乱。
即使该类位于UIViewControllerRepresentable
结构体中,SwiftUI也不会自动将其用于视图的协调器。相反,我们需要添加一个名为makeCoordinator()
的新方法,如果实现该方法,SwiftUI将自动调用该方法。所有这些需要做的就是创建并配置我们的Coordinator
类的实例,然后将其返回。
现在,我们的Coordinator
类并没有做任何特别的事情,因此我们可以通过将以下方法添加到ImagePicker
结构体中来将其返回:
func makeCoordinator() -> Coordinator {
Coordinator()
}
到目前为止,我们要做的是创建一个ImagePicker
结构体,该结构知道如何创建UIImagePickerController
,现在我们刚刚告诉ImagePicker
,它应该有一个协调器来处理来自UIImagePickerController
的通信。
下一步是告诉UIImagePickerController
,当发生某些事情时,应该告诉我们的协调器。这仅需要makeUIViewController()
中的一行代码,因此请将其直接添加到return picker
这一行之前:
picker.delegate = context.coordinator
该代码无法编译,但是在我们修复它之前,我想花一点时间来探究刚刚发生的事情。我们不会自己调用makeCoordinator()
;创建ImagePicker
的实例时,SwiftUI会自动调用它。甚至更好的是,SwiftUI自动将其创建的协调器与我们的ImagePicker
结构体相关联,这意味着当它调用makeUIViewController()
和updateUIViewController()
时,它将自动将该协调器对象传递给我们。
因此,我们刚刚编写的代码行告诉Swift使用刚刚制作的协调器作为UIImagePickerController
的委托。这意味着在图像选择器控制器内部发生任何事情时(即,当用户选择图像时),它将向我们的协调器报告该操作。
我们的代码无法编译的原因是,Swift正在检查我们的协调器类是否能够充当UIImagePickerController
的委托,发现它没有,因此拒绝进一步构建我们的代码。要解决此问题,我们需要从我们原来的Coordinator
类修改为:
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
这做三件事:
- 它使类继承自
NSObject
,NSObject
是UIKit中几乎所有内容的父类。NSObject
允许Objective-C询问对象在运行时支持什么功能,这意味着图像选择器可以说“嘿,用户选择了图像,您想做什么?”之类的话。 - 它使该类符合
UIImagePickerControllerDelegate
协议,该协议添加了用于检测用户何时选择图像的功能。(NSObject
使Objective-C可以检查功能;该协议实际上是它提供的。) - 它使该类符合
UINavigationControllerDelegate
协议,该协议使我们可以检测用户何时在图像选择器的屏幕之间移动。
现在您可以看到为什么需要为Coordinator
使用类:我们需要从NSObject
继承,以便Objective-C可以查询我们的协调器以查看其支持的功能。
至此,我们有了一个包装了UIImagePickerController的ImagePicker
结构体,并且我们已经配置了该图像选择器控制器,以便在发生有趣的事情时与我们的Coordinator
类进行对话。
UIImagePickerControllerDelegate
协议定义了我们可以实现的两种可选方法:一种用于用户选择图像时,另一种用于用户按下cancel
时。如果我们不实现cancel
方法,则UIKit会自动关闭图像选择器控制器,因此我们可以不必理会。但是“选择图像”方法很重要:我们需要抓住它并对该图像做些事情。问题是我们应该怎么做?
如果我们将UIKit放在一边并考虑纯粹的功能,我们想要的是ImagePicker
将该图像报告回最初使用选择器的对象。我们将在ContentView
结构体中的工作表内显示ImagePicker
,因此我们希望将所选的图像都提供给它,然后关闭工作表。
这里我们需要的是SwiftUI的@Binding
属性包装器,它允许我们创建从ImagePicker
到创建它的对象的绑定。这意味着我们可以在图像选择器中设置绑定值,并让它实际更新存储在其他位置(例如在ContentView
中)的值。
因此,将此属性添加到ImagePicker
:
@Binding var image: UIImage?
当您在做那种操作时,我们还希望在选择图片后关闭该视图。现在,我们根本不处理图像选择,因此我们获得了UIKit的默认关闭行为,但是一旦注入一些自定义功能,我们就需要手动处理关闭。
因此,将第二个属性添加到ImagePicker
,以便我们可以以编程方式关闭视图:
@Environment(\.presentationMode) var presentationMode
现在,我们只是将这些属性添加到ImagePicker
,但是我们需要在Coordinator
类中使用它们,因为这是在选择图像时会通知的属性。
一个更好的主意不是将数据向下传递一个级别,而是告诉协调器其父级是什么,以便它可以在那里直接修改值。这意味着向Coordinator
类添加ImagePicker
属性和关联的初始化器,如下所示:
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
现在我们可以修改makeCoordinator()
,使其将ImagePicker
结构体传递给协调器,如下所示:
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
此时,您的整个ImagePicker
结构体应如下所示:
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
@Binding var image: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
最后,我们准备好实际读取UIImagePickerController
的响应,这是通过实现一个具有非常特定名称的方法来完成的。由于UIKit是图像选择器的委托,因此UIKit将在我们的Coordinator
类中查找此方法,如果找到该方法,则将对其进行调用。
方法名称很长,必须正确正确才能让UIKit找到它,但是Xcode可以帮助我们解决自动完成问题。因此,进入Coordinator
类并开始输入以下内容:“didFinishPicking” –当然不带引号。Xcode的代码完成应仅提供一种方法,如果选择它,您将获得以下代码:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
code
}
该方法接收一个字典,其中键的类型为UIImagePickerController.InfoKey
,而值的类型为Any
。我们的工作就是挖掘这些信息,以找到选定的图像,将其分配给我们的父级,然后关闭图像选择器。
因此,用以下代码替换“code”占位符:
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
请注意,我们需要如何为UIImage
进行类型转换——这是因为我们提供的字典包含各种数据类型,因此我们需要小心。
到目前为止,我敢打赌您确实错过了SwiftUI的漂亮简单性,因此您将很高兴地知道我们终于完成了ImagePicker
结构体的工作——它可以完成我们现在需要的一切。
因此,最后我们可以返回到 ContentView.swift。这是我们早些时候留下的代码:
struct ContentView: View {
@State private var image: Image?
@State private var showingImagePicker = false
var body: some View {
VStack {
image?
.resizable()
.scaledToFit()
Button("Select Image") {
self.showingImagePicker = true
}
}
.sheet(isPresented: $showingImagePicker) {
ImagePicker()
}
}
}
为了将ImagePicker
视图集成到其中,我们需要首先添加另一个可以传递到选择器中的@State
图像属性:
@State private var inputImage: UIImage?
接下来,我们需要一个可以在属性更改时调用的方法。记住,这里不能使用普通的属性观察器,因为Swift会忽略它,所以我们将编写一个方法来检查inputImage
是否具有值,以及是否使用它来为image
属性分配新的Image
视图。。
立即将此方法添加到 ContentView
:
func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
最后,我们需要以两种方式更改sheet()
修饰符:
- 我们需要将
inputImage
属性传递到我们的图像选择器中,因此在选择图像时将对其进行更新。 - 当工作表被关闭时,我们需要调用新的
loadImage()
方法。
第一项任务很简单,只需将工作表的内容更改为此:
ImagePicker(image: self.$inputImage)
第二项任务要求您学习一些新知识:可以将额外的onDismiss
参数传递给sheet()
修饰符,该参数使我们可以指定一个在关闭图纸时运行的函数。我们要调用loadImage()
,因此我们应该将sheet()
修饰符更新为:
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
我们完成了!继续运行该应用程序并尝试一下——您应该可以点击按钮,浏览您的照片库,然后选择一张照片。发生这种情况时,图像选择器视图应消失,并且您选择的图像将显示在下面。
我意识到此时您可能已经讨厌UIKit和协调器,但是在继续之前,我想总结一下完整的过程:
- 我们创建了一个符合
UIViewControllerRepresentable
的SwiftUI视图。 - 我们给了它一个
makeUIViewController()
方法,该方法创建了某种UIViewController
,在我们的示例中为UIImagePickerController
。 - 我们添加了一个嵌套的
Coordinator
类,以充当UIKit视图控制器和SwiftUI视图之间的桥梁。 - 我们为协调器提供了
didFinishPickingMediaWithInfo
方法,该方法将在选择图像时由UIKit触发。 - 最后,我们为
ImagePicker
提供了@Binding
属性,以便它可以将更改传递给父视图。
值得一提的是,使用协调器一次之后,第二次及以后的工作会更容易,但是如果您发现整个系统现在很混乱,我也不会怪您。
不必担心太多——我们很快会再次谈到这一点,然后在以后的项目中再讲。您将有足够的机会练习!