[SwiftUI 100 天] 使用 Coordinator 管理 SwiftUI 的视图控制器
2020/5/21 23:26:52
本文主要是介绍[SwiftUI 100 天] 使用 Coordinator 管理 SwiftUI 的视图控制器,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
前面我们学习了如何用 UIViewControllerRepresentable
封装 UIKit 视图控制器,以便它们可以被 SwiftUI 使用。我们还特别聚焦了 UIImagePickerController
。不过,我们也遇到一个问题:尽管可以显示图像选择器,用户选择图像后我们没有被通知到。
对此,SwiftUI 的解决方案是 coordinators,它对来自 UIKit 背景的同学可能带来混淆,因为他们也有一个叫 coordinators 的设计模式,当扮演的是完全不同的角色。明确一下,SwiftUI 的 coordinators 和众多 UIKit 开发者使用的 coordinator 模式一点关系都没有。如果你之前用过这个模式,需要暂且把它从大脑中请出去,以免困惑。
SwiftUI 的 coordinators 被设计来扮演 UIKit 视图控制器的委托的角色。记住,“委托” 是那些对各处发生的事件做出响应的对象。举个例子,UIKit 让我们附着一个委托对象到文本框视图,每当用户输入任何东西时,委托都会被通知。这意味着,UIKit 开发者可以在不创建自定义文本框类型的前提下,修改文本框的工作方式。
在 SwiftUI 中使用 coordinator 要求你了解一些 UIKit 的工作方式,因此集成 UIKit 的视图控制器在意料之中。为了演示这一点,我们要升级 ImagePicker
视图,以便返回用户选择的图像。
下面是改造之前的代码:
struct ImagePicker: UIViewControllerRepresentable { func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { let picker = UIImagePickerController() return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) { } }复制代码
我们会一步一步来,因为工作量其实不小 —— 如果代码费了你不少时间理解,不要感觉沮丧,因为第一次遇到 coordinator 时,它们的确是个难点。
首先,在 ImagePicker
结构体中写一个嵌套类:
class Coordinator { }复制代码
你稍后会知道为什么这里必须使用类而不是结构体。虽然不要求一定是嵌套类,不过使用嵌套类的好处是能够使代码封装更整洁,否则你可能会迷失在许多视图控制器和协调器的混杂之中。
尽管这个类处于 UIViewControllerRepresentable
结构体中,SwiftUI 不会自动应用这个类作为视图的 coordinator。相反,我们需要添加一个叫 makeCoordinator()
的新方法,如果有实现这个方法,SwiftUI 会自动调用它。我们需要做的是创建一个 Coordinator
类的实例并返回。
现在 Coordinator
类还没有具体的功能,我只要简单返回就好了:
func makeCoordinator() -> Coordinator { Coordinator() }复制代码
下一步是让 UIImagePickerController
知道,当事件发生时,让委托的 coordinator 处理。这只需要一行代码,添加下面这行代码:
picker.delegate = context.coordinator复制代码
只有这行代码是无法编译通过的。在修复之前,我需要一些篇幅来解释发生的事情。我们不会自行调用 makeCoordinator()
;SwiftUI 会在 ImagePicker
创建时自动做这个动作。更好的是,SwiftUI 会自动关联协调器和我们的 ImagePicker
结构体,其含义是:当它调用 makeUIViewController()
和 updateUIViewController()
时,它会自动传递协调器对象给我们。
因此,上面那行代码告诉 Swift 采用协调器作为 UIImagePickerController
的委托,也就是说,当 image picker 控制器内部发生变化的时候 —— 比如用户选择了一张图片 —— 它会将这个动作报告给我们的协调器。
代码无法编译通过的原因在于 Swift 检查我们的协调器类,发现它无法扮演 UIImagePickerController
, 委托的角色。为了修复这个问题,我们需要修改 Coordinator
类:
class Coordinator {复制代码
变为:
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {复制代码
这样一来带来三个变化:
- 使得类继承
NSObject
,它几乎是 UIKit 里一切东西的父类。NSObject
使得 Objective-C 能够向对象询问其支持的运行时功能,也就是说,image picker 可以发出这些的提问 “现在用户选择了一张图片,你打算怎么做?” - 使得类遵循
UIImagePickerControllerDelegate
协议,这个协议添加了侦测到用户选择图片后的应对功能(NSObject
让 Objective-C 检查功能,而这个协议实际提供功能) - 使得类遵循
UINavigationControllerDelegate
协议,这个协议能够侦测到用户在 image picker 的不同屏之间移动的行为。
现在你明白为什么我们需要用到一个 Coordinator
类了吧:我们需要继承自 NSObject
以便 Objective-C 能够查询我们支持的功能。
至此,我们实现了一个 ImagePicker
结构体,内部封装了一个 UIImagePickerController
。我们配置这个控制器在感兴趣的事件发生时和 Coordinator
类交互。
UIImagePickerControllerDelegate
协议定义了两个可选方法给我们实现:一个用于用户选择图片后的处理,一个用于用户点击取消后的处理。如果我们不实现取消的处理方法,UIKit 会自动关闭 image picker 控制器,所以我们可以跳过这个方法。但选择图片的处理方法很重要:我们需要捕捉选中的图片并且拿去使用。问题是,我们该怎么做?
我们暂且把 UIKit 放到一边,从功能的角度单纯地思考一下。我们实现 ImagePicker
的功能是为了拿到图片,而我们是在 ContentView
结构体里通过 sheet 呈现的 ImagePicker
。因此,我们得拿到图片,然后关掉 sheet。
这里需要用到的 SwiftUI 的 @Binding
属性包装器,它能让我们创建一个从 ImagePicker
到任何创建这个 ImagePicker
的东西的绑定。也就是说,我们在 image picker 里设置绑定值,但这个值更新的实际值时存储在别的地方的 —— 例如,我们的 ContentView
。
把这个属性添加到 ImagePicker
:
@Binding var image: UIImage?复制代码
添加完属性,我们还希望图片选择之后关闭 sheet。刚才我们还不处理图片选择的逻辑,因此默认的逻辑是 UIKit 提供的,即关闭视图,但是一旦我们注入自定义的函数,我们就需要手动自行处理关闭逻辑了。
把下面这个属性也添加到 ImagePicker
,以便我们可以通过程序化手段关闭视图:
@Environment(\.presentationMode) var presentationMode复制代码
现在我们已经把属性添加到 ImagePicker
,但图像选择首先通知的是 Coordinator
类。
相比于直接把数据再往下一层放进 Coordinator
类,更好的方式是告诉协调器它的父对象是谁,让它直接透过引用修改这些数据。给 Coordinator
类添加属性并修改构造器:
var parent: ImagePicker init(_ parent: ImagePicker) { self.parent = parent }复制代码
相应地修改 makeCoordinator()
:
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 会在我们的 Coordinator
类中寻找这个方法,因为它是 image picker 委托类。如果方法被找到,则会被调用。
方法名很长,你可以利用 Xcode 的代码补全。在 Coordinator
类里,输入:“didFinishPicking”,Xcode 的代码补全应该会提供一个精确的方法,选择它,得到下面的代码:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { code }复制代码
这个方法接收一个字典,其中的键是 UIImagePickerController.InfoKey
类型,值是 Any
类型。从中找到被用户选择的图片是我们的任务。我们找到它,赋给 parent,然后关闭 image picker。
把 “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
图片属性,它是传给 image picker 用的:
@State private var inputImage: UIImage?复制代码
接下来,我们要实现一个方法,用于加载 inputImage
。
把下面这个方法添加到 ContentView
:
func loadImage() { guard let inputImage = inputImage else { return } image = Image(uiImage: inputImage) }复制代码
最后,修改 sheet()
modifier:
- 我们需要把
inputImage
属性传给 image picker,以便用户选择图片后更新给它。 - 我们还需要在 sheet 关闭后调用
loadImage()
方法。
第一个任务很简单:
ImagePicker(image: self.$inputImage)复制代码
第二个任务要求你了解一些新知识:一个可以传给 sheet()
modifier 的额外的 onDismiss
参数,它可以让我们指定一个 sheet 关闭时执行的方法。这里,我们调用 loadImage()
:
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {复制代码
这样我们就完工了。运行应用,尝试一下 —— 你可以点击按钮,浏览相册并从中选取照片。当照片被选择后,image picker 视图会消失,而你选择的图片将会被显示。
我能想象,到这里你可能觉得 UIKit 和协调器这些东西有点繁琐了。在我们继续之前,我简单总结一下整个过程:
- 我们创建一个遵循
UIViewControllerRepresentable
的 SwiftUI 视图 - 提供
makeUIViewController()
方法以创建某种UIViewController
,在我们的例子中是UIImagePickerController
。 - 添加一个嵌套的
Coordinator
类,扮演 UIKit 视图控制器和我们的 SwiftUI 视图之间的桥梁。 - 给协调器提供一个
didFinishPickingMediaWithInfo
方法,它会在图片被选择时由 UIKit 触发。 - 最后,我们给
ImagePicker
一个@Binding
属性,以便它能把变化发回父视图。
值得一提的是,在你首次接触过协调器之后,后续的使用应该会简单一些。如果你对这整套东西感到困惑,我不会感觉意味。
不过别担心, 我们在后续的项目里还会用到这套东西,你有足够的练习机会!
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
这篇关于[SwiftUI 100 天] 使用 Coordinator 管理 SwiftUI 的视图控制器的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值