SwiftUI学习100天(Day64 - 项目 13,第三部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

信不信由你,在我们进入实施阶段之前,我们还有一天的时间来研究这个项目的技术,我把最难的事情留到了最后。
今天,你将了解 SwiftUI 如何与 UIKit 协同工作的一些复杂情况。如果你之前曾经使用过 UIKit,那么这些事情不会太费力,但如果 UIKit 对你来说是新手,那么今天可能会像你第一次在 Swift 中遇到闭包时一样受到伤害。对真的。
坚持下去!今天之后我们将开始将所有这些概念付诸实践,这样你就离真正有趣的部分很近了。从邮票中汲取灵感——正如作家乔什·比林斯 (Josh Billings) 曾经打趣道,“它的用处在于能够坚持一件事,直到它到达那里。”
今天你只有两个主题要完成,你将在其中了解协调器、委托、NSObject
选择@objc
器和其他在夜间出现的问题。

使用协调器管理 SwiftUI 视图控制器
之前我们研究了如何使用UIViewControllerRepresentable
包装 UIKit 视图控制器,以便它可以在 SwiftUI 中使用,特别关注PHPickerViewController
. 然而,我们遇到了一个问题:虽然我们可以显示图像选择器,但我们无法响应用户选择图像或按下取消。
SwiftUI 对此的解决方案称为协调器,这对于来自 UIKit 背景的人来说有点混乱,因为我们有一个设计模式也称为协调器,它执行完全不同的角色。需要明确的是,SwiftUI 的协调器与许多开发人员在 UIKit 中使用的协调器模式完全不同,因此如果你以前使用过该模式,请将其从你的大脑中抛弃以避免混淆!
SwiftUI 的协调器旨在充当 UIKit 视图控制器的委托。请记住,“委托”是响应其他地方发生的事件的对象。例如,UIKit 允许我们将委托对象附加到其文本字段视图,当用户键入任何内容、按下回车键等时,该委托将收到通知。这意味着 UIKit 开发人员可以修改他们的文本字段的行为方式,而无需创建他们自己的自定义文本字段类型。
在 SwiftUI 中使用协调器需要你了解一些 UIKit 的工作方式,这并不奇怪,因为我们实际上是在集成 UIKit 的视图控制器。因此,为了演示这一点,我们将升级我们的ImagePicker
视图,以便它可以在用户选择图像或按下取消时进行报告。
提醒一下,这是我们现在拥有的代码:
我们将一步一步来,因为这里有很多东西要吸收——如果你需要一些时间来理解,不要难过,因为协调员在你第一次遇到他们时真的不容易。
首先,在结构中添加这个嵌套类ImagePicker
:
是的,它需要是一个类,你稍后会看到。它不需要是一个嵌套类,尽管这是一个好主意,因为它巧妙地封装了功能——如果没有嵌套类,如果你有很多视图控制器和协调器都混在一起,那将会很混乱。
即使该类位于UIViewControllerRepresentable
结构内部,SwiftUI 也不会自动将其用于视图的协调器。相反,我们需要添加一个名为 makeCoordinator()
的新方法
,如果我们实现它,SwiftUI 会自动调用它。所有这一切需要做的就是创建和配置我们Coordinator
类的实例,然后将其返回。
现在我们的Coordinator
类没有做任何特别的事情,所以我们可以通过将这个方法添加到ImagePicker
结构中来发送一个:
到目前为止我们所做的是创建一个ImagePicker
结构体,他知道如何创建
现在我们只是告诉PHPickerViewController
,ImagePicker
它应该有一个协调器来处理来自那个PHPickerViewController
的通信
。
下一步是告诉PHPickerViewController
当事情发生时它应该告诉我们的协调员。这只需要 makeUIViewController()
中的一行代码
,所以直接在该return picker
行之前添加:
该代码无法编译,但在我们修复它之前,我想花点时间深入了解刚刚发生的事情。
我们不称呼makeCoordinator()
自己;SwiftUI 在创建 ImagePicker
的实例时自动调用它
。更好的是,SwiftUI 自动将它创建的协调器与我们的ImagePicker
结构体相关联,这意味着当它调用makeUIViewController()
时
,updateUIViewController()
会自动将该协调器对象传递给我们。
因此,我们刚刚编写的代码行告诉 Swift 使用刚刚创建的协调器作为PHPickerViewController
. 这意味着只要照片选择器控制器内部发生某些事情——即,当用户选择图像或按下取消——它就会将该操作报告给我们的协调器。
我们的代码无法编译的原因是 Swift 正在检查我们的协调器类是否能够充当 PHPickerViewController
的委托
,发现它不能,因此拒绝进一步构建我们的代码。为了解决这个问题,我们需要修改我们的Coordinator
类:
对此:
这做了三件事:
它使类继承自
NSObject
,这是 UIKit 中几乎所有内容的父类。NSObject
允许 Objective-C 在运行时询问对象它支持什么功能,这意味着照片选择器可以说“嘿,用户选择了一张图像,你想做什么?”它使类符合
PHPickerViewControllerDelegate
协议,这就是添加检测用户何时选择图像的功能。(NSObject
让 Objective-C检查功能;这个协议实际上提供了它。)它会阻止我们的代码编译,因为我们已经说过该类符合
PHPickerViewControllerDelegate
但我们还没有实现该协议所需的一个方法。
尽管如此,至少现在你可以明白为什么我们需要使用类 Coordinator
:我们需要继承自NSObject
,
以便 Objective-C 可以查询我们的协调器以查看它支持什么功能。
在这一点上,我们有一个ImagePicker
的结构体,它包装了一个PHPickerViewController
,并且我们已经配置了图像选择器控制器,以便在发生有趣的事情时与我们的Coordinator
类交谈。
最后一步是实现PHPickerViewControllerDelegate
协议的一个必需方法,当用户完成选择时将调用该方法。这可能意味着我们有一张图片,或者用户按下了取消键,所以我们需要做出适当的响应。
如果我们暂时将 UIKit 放在一边并考虑纯功能,我们想要的是ImagePicker
将该图像报告给首先使用选择器的任何人。我们在ContentView
的结构体的工作表中展示ImagePicker
,我们希望无论选择什么图像都可以得到它,然后关闭工作表。
我们在这里需要的是 SwiftUI 的@Binding
属性包装器,它允许我们创建一个绑定ImagePicker
到任何创建它的对象。这意味着我们可以在我们的图像选择器中设置绑定值,并让它实际更新存储在其他地方的值——在ContentView
中,
例如。
因此,将此属性添加到ImagePicker
:
现在,我们刚刚将该属性添加到ImagePicker
,但我们需要在我们的Coordinator
类中访问它,
因为这是在选择图像时将被通知的那个。
与其将数据向下传递一个级别,不如告诉协调器它的父级是什么,这样它就可以直接修改那里的值。这意味着向类添加一个ImagePicker
属性和关联的初始化程序Coordinator
,如下所示:
现在我们可以修改makeCoordinator()
,以便它将ImagePicker
结构传递给协调器,如下所示:
此时你的整个ImagePicker
结构应该是这样的:
终于,我们准备好实际读取来自我们PHPickerViewController
的响应
,这是通过实现一个具有非常特定名称的方法来完成的。Swift 将在我们的Coordinator
类中查找此方法
,因为它是图像选择器的委托,因此请确保将其添加到此处。
方法名称很长,需要完全正确才能让 UIKit 找到它,但 Xcode 可以帮助我们完成自动完成。因此,单击错误行上的红色六边形,然后单击“修复”以添加此方法存根:
该方法接收我们关心的两个对象:用户与之交互的选择器视图控制器,以及用户选择的数组,因为可以让用户一次选择多个图像。
我们的工作是做三件事:
告诉选择器解雇自己。
如果用户没有做出选择则退出——如果他们点击了取消。
否则,查看用户的结果是否包含
UIImage
我们可以实际加载的内容,如果是,则将其放入parent.image
属性中。
因此,将“代码”占位符替换为:
注意我们如何需要类型转换UIImage
——那是因为我们提供的数据在理论上可以是任何东西。是的,我知道我们特别要求提供照片,但PHPickerViewControllerDelegate
对任何类型的媒体都调用相同的方法,这就是我们需要小心的原因。
在这一点上,我敢打赌你真的错过了 SwiftUI 的美丽简单,所以你会很高兴知道我们终于完成了ImagePicker
结构——它做了我们现在需要的一切。
所以,最后我们可以返回到 ContentView.swift。这是我们之前离开它的方式:
要将我们的ImagePicker
视图集成到其中,我们需要首先添加另一个@State
可以传递到选择器中的图像属性:
我们现在可以更改我们的sheet()
修改器以将该属性传递给我们的图像选择器,因此它会在选择图像时更新:
接下来,我们需要一个可以在该属性更改时调用的方法。请记住,我们不能在这里使用普通的属性观察器,因为 Swift 会忽略对绑定的更改,因此我们将编写一个方法来检查inputImage
是否
有值,如果有,则使用它为image
属性分配一个新
.Image
视图
将此方法添加到ContentView
现在:
现在我们可以使用onChange()
修饰符在选择新图像时调用
loadImage()
- 将其放在
sheet()
修饰符下方:
我们完成了!继续运行该应用程序并尝试一下——你应该能够点击按钮,浏览你的照片库,然后选择一张照片。当发生这种情况时,照片选择器应该会消失,你选择的图像将显示在下方。
我意识到此时你可能厌倦了 UIKit 和协调器,但在我们继续之前我想总结一下完整的过程:
我们创建了一个符合
UIViewControllerRepresentable
的SwiftUI视图
.我们给了它一个
makeUIViewController()
的方法
创建某种UIViewController
,
在我们的示例中是一个PHPickerViewController
.我们添加了一个嵌套
Coordinator
类作为 UIKit 视图控制器和我们的 SwiftUI 视图之间的桥梁。我们为该协调器提供了一个
didFinishPicking
方法,该方法将在选择图像时由 iOS 触发。最后,我们给了我们
ImagePicker
一个@Binding
属性,以便它可以将更改发送回父视图。
对于它的价值,在你使用过协调器一次之后,第二次和随后的时间会更容易,但是如果你现在发现整个系统非常莫名其妙,我不会责怪你。
别太担心——我们很快就会再次回到这个话题,所以你会有足够的时间练习。这也意味着你不应该删除你的 ImagePicker.swift 文件,因为这是你将在这个项目和你创建的其他项目中使用的另一个有用的组件。



如何将图像保存到用户的照片库
在我们完成这个项目的技术之前,我们需要解决最后一个 UIKit 的乐趣:一旦我们处理了用户的图像,我们就会返回UIImage
,但是我们需要一种方法来将处理过的图像保存到用户的照片库。这使用了一个名为 UIImageWriteToSavedPhotosAlbum
()的 UIKit 函数
,其最简单的形式使用起来很简单,但为了使其有用,你需要重新研究 UIKit。至少它会让你真正体会到 SwiftUI 有多好!
在我们编写任何代码之前,我们需要做一些新的事情:我们需要为我们的项目添加一个配置选项。我们构建的每个项目都有一大堆这样的东西,描述我们支持的界面方向、我们应用程序的版本号和其他固定数据。这不是代码:这些选项必须全部提前声明,在一个单独的文件中,这样系统就可以读取它们而无需运行我们的应用程序。
这些选项都位于 Xcode 中的特定位置,除非你知道自己在做什么,否则很难找到:
在 Project Navigator 中,选择树中的顶部项目。它将包含你的项目名称 Instafilter。
你会看到 Instafilter 列在 PROJECT 和 TARGETS 下。请在目标下选择它。
现在你会在顶部看到一堆选项卡,包括“常规”、“签名和功能”等 - 从那里选择“信息”。
你可以在此处为你的项目添加一系列配置选项,但现在我们需要一个特定选项。你看,写入照片库是一项受保护的操作,这意味着我们不能在没有用户明确许可的情况下执行此操作。iOS 将负责请求许可并检查响应,但我们需要提供一个简短的字符串来向用户解释我们为什么要首先写入图像。
要添加你的权限字符串,请右键单击任何现有选项,然后选择添加行。你会看到一个可供选择的选项下拉列表——我希望你向下滚动并选择“隐私 - 照片库添加使用说明”。对于右侧的值,请输入文本“我们要保存过滤后的照片”。
完成后,我们现在可以使用该UIImageWriteToSavedPhotosAlbum()
方法写出图片。我们之前的工作中已经有了这个loadImage()
方法:
我们可以修改它,让它立即保存加载的图像,有效地创建一个副本。将此行添加到方法的末尾:
就是这样 - 每次你导入图像时,我们的应用程序都会将其保存回照片库。第一次尝试时,iOS 会自动提示用户写照片的权限,并显示我们添加到配置选项中的字符串。
现在,你可能会看着它并认为“这很简单!” 你是对的。但之所以容易,是因为我们做了尽可能少的工作:我们将要保存的图像作为第一个参数提供给UIImageWriteToSavedPhotosAlbum()
,然后nil
作为其他三个参数提供。
这些nil
参数很重要,或者至少前两个很重要:它们告诉 Swift 在保存完成时应该调用什么方法,这反过来会告诉我们保存操作是成功还是失败。如果你不关心那个,那你就完了——三个值都是nil
是可以的。但请记住:用户可以拒绝访问他们的照片库,因此如果你没有发现保存错误,他们会想知道为什么你的应用无法正常运行。
UIKit 需要两个参数来知道要调用哪个函数的原因是因为这段代码很旧——比 Swift 老得多,事实上,它甚至比 Objective-C 的闭包等价物还要早。因此,它使用了一种完全不同的函数引用方式:在引用第一个nil
时,
我们应该指向一个对象,在引用第二个nil
时,
我们应该指向要调用的方法的名称。
如果这听起来很糟糕,恐怕你只知道一半的故事。你看,这两个参数都有其自身的复杂性:
我们提供的对象必须是类,而且必须继承自
NSObject
。这意味着我们无法指向 SwiftUI 视图结构。该方法作为方法名称提供,而不是实际方法。Objective-C 使用此方法名称在运行时查找实际代码,然后可以运行这些代码。该方法需要有一个特定的签名(参数列表),否则我们的代码将无法工作。
但是等等:还有更多!出于性能原因,Swift 不喜欢以 Objective-C 可以读取的方式生成代码——整个“在运行时查找方法”的东西非常简洁,但也非常慢。因此,在编写方法名称时,我们需要做两件事:
使用一个名为
#selector
的特殊编译器指令标记该方法
,它要求 Swift 确保方法名称存在于我们所说的位置。添加一个调用
@objc
到方法的属性,它告诉 Swift 生成可以被 Objective-C 读取的代码。
你知道的,在我转向 SwiftUI 之前我写了十多年的 UIKit 代码,并且已经写下所有这些解释使得这个旧的 API 看起来像是反人类罪。可悲的是,它就是这样,我们坚持下去。
在向你展示代码之前,我想提一下第四个参数。所以,第一个是要保存的图像,第二个是应该通知保存结果的对象,第三个是应该运行的对象上的方法,然后是第四个。我们不会在这里使用它,但你需要知道它的作用:我们可以在这里提供任何类型的数据,当我们的完成方法被调用时,它会传回给我们。
这就是 UIKit 所说的“上下文”,它可以帮助你将一个图像保存操作与另一个图像保存操作区分开来。你可以在这里提供任何你想要的东西,所以 UIKit 使用你能想象到的最不干涉的类型:一块原始的内存块,Swift 对此不做任何保证。这在 Swift 中有其自己的特殊类型名称:UnsafeRawPointer
。老实说,如果不是因为我们不得不在这里使用它,我甚至不会告诉你它的存在,因为在你的应用程序开发生涯的这个阶段,它并不是真正有用的。
无论如何,这已经绰绰有余了。在你决定放弃这个项目并直接进入下一个项目之前,让我们把它结束并完成。
正如我所说,要将图像写入照片库并读取响应,我们需要某种继承自NSObject
. 在里面,我们需要一个带有精确签名的方法,并用 @objc
标记
,然后我们可以从 UIImageWriteToSavedPhotosAlbum()
调用它
。
将所有这些放在一起,请创建一个名为 ImageSaver.swift 的新 Swift 文件,将其基础导入更改为 SwiftUI,然后为其提供以下代码:
有了它,我们现在可以从 ContentView
中使用它
,如下所示:
如果你现在运行代码,你应该看到“保存完成!” 选择图像后会立即输出消息,但你还会看到系统提示你授予写入照片库的权限。
是的,考虑到它需要多少解释,这是非常少的代码,但从好的方面来说,完成了这个项目的概述,所以在很长一段时间(很长,很长!)最后我们可以进入实际的实现。
请继续并将你的项目恢复到默认状态,这样我们就有了一个干净的工作状态,但我希望你保留ImagePicker
和ImageSaver
- 这两个项目稍后都会用到,它们在其他项目中也很有用你将来可能创建的项目。


