SwiftUI学习100天(Day72 - 项目 14,第五部分)

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

今天是这个项目编码的最后一天,我相信你期待着明天的挑战和复习——它应该从这么长的教程中做出很好的改变。
不过,首先,我们需要涵盖两个棘手的主题,其中一个将真正挑战你,因为我们将重构我们的代码以使用 MVVM 设计模式。正如你将看到的,这有助于在我们的项目中将逻辑与布局分开,但这也需要一些思考——尤其是因为你需要了解主要参与者的概念。
当你完成今天的工作时,你可能真的开始感觉到难度曲线在上升,因为我们的项目在增长,规模和复杂性都在增加。我想借此机会提醒你几件事:
你不是一个人; 每个人都必须经历同样的学习曲线。
这是一场马拉松,而不是短跑;慢慢来,它会来的。
休息一下,稍后再讨论一个话题是可以的;有新鲜的眼睛会有所帮助。
没有奋斗就没有学问;如果你正在努力学习一些东西,它最终会坚持得更好。
孔子有一句名言,你最好记住:“走得慢不重要,只要不停。”
今天你有两个主题需要完成,其中你将学习如何安全地将数据写入磁盘,以及如何启用生物识别身份验证。

将 MVVM 引入你的 SwiftUI 项目
到目前为止,我已经向你介绍了 Swift 和 SwiftUI 的一系列概念,并且我还提供了一些关于如何更好地组织代码的技巧。好吧,在这里我想进一步探讨后一部分:我们将研究通常称为软件架构的东西,或者更宏大的名称架构设计模式——实际上它只是一种构建代码的特殊方式。
我们要查看的模式称为 MVVM,它是 Model View View-Model 的首字母缩写词。这是一个非常糟糕的名字,并且彻底混淆了人们,但恐怕我们现在还是坚持使用它。什么是 MVVM 没有单一的定义,你会发现网上有各种各样的人在争论它,但这没关系——在这里我们将保持简单,并使用 MVVM 作为获取我们程序的一些方式我们的视图结构中的状态和逻辑。实际上,我们将逻辑与布局分开。
我们将边走边探索这个定义,但现在让我们从大事开始:创建一个名为 ContentView-ViewModel.swift 的新 Swift 文件,然后为它额外导入MapKit 。我们将使用它来创建一个新类来管理我们的数据,并代表ContentView
结构对其进行操作,这样我们的视图就不会真正关心底层数据系统的工作方式。
我们将从三件微不足道的事情开始,然后从那里开始。首先,创建一个符合ObservableObject
协议的新类,这样我们就可以将更改报告回正在监视的任何 SwiftUI 视图:
其次,我希望你将该新类放在上的扩展中ContentView
,如下所示:
现在我们说这不仅仅是任何视图模型,它是ContentView
. 稍后,你将负责添加第二个视图模型来处理EditView
,因此你可以尝试查看概念如何映射到其他地方。
我希望你做的最后一个小改动是向@MainActor
整个类添加一个新属性 ,如下所示:
主要参与者负责运行所有用户界面更新,并将该属性添加到类意味着我们希望它的所有代码——任何时候它运行任何东西,除非我们特别要求——在那个主要参与者上运行。这很重要,因为它负责进行 UI 更新,而这些更新必须发生在主要参与者身上。在实践中这并不是那么容易,但我们稍后会谈到这一点。
现在,我们以前使用过ObservableObject
类,但没有@MainActor
--他们
怎么会起作用?好吧,无论何时我们使用@StateObject
或@ObservedObject
Swift 都在幕后默默地为我们推断属性@MainActor
——它知道这两者都意味着 SwiftUI 视图依赖于外部对象来触发其 UI 更新,因此它将确保所有工作自动发生在没有我们要求的主要演员。
但是,这并不能提供 100% 的安全性。是的,当从 SwiftUI 视图使用时,Swift 会推断出这一点,但是如果你从其他地方访问你的类 - 例如,从另一个类?然后代码可以在任何地方运行,这是不安全的。因此,通过在@MainActor
此处添加属性,我们采用了一种“腰带和大括号”方法:我们告诉 Swift 此类的每个部分都应该在主要参与者上运行,因此更新 UI 是安全的,无论它在哪里使用。
现在我们已经有了我们的类,我们可以从我们的视图中选择哪些状态应该被移动到视图模型中。有些人会告诉你移动所有这些,其他人会更有选择性,这没关系——再说一次,MVVM 没有单一的样子,所以我将为你提供工具和知识来自己试验。
让我们从简单的事情开始:将所有三个@State
属性移到它的ContentView
视图模型中,@State private
只是切换@Published
——它们不能再私有了,因为它们明确需要共享ContentView
:
ContentView
现在我们可以用一个属性替换所有这些属性:
这当然会破坏很多代码,但修复很容易——只需viewModel
在不同的地方添加即可。所以,$mapRegion
变成$viewModel.mapRegion
,locations
变成viewModel.locations
,等等。
一旦你在任何需要它的地方添加了它,你的代码就会再次编译,但你可能想知道这有什么帮助——我们不是只是将代码从一个地方移动到另一个地方吗?嗯,是的,但是有一个重要的区别会随着你的技能的增长而变得更加清晰:将所有这些功能都放在一个单独的类中使得为你的代码编写测试变得更加容易。
视图在处理数据表示时效果最好,这意味着数据操作是将代码移入视图模型的理想选择。考虑到这一点,如果你仔细查看你的ContentView
代码,你可能会注意到我们的视图在两个地方做了比它应该做的更多的工作:添加一个新位置和更新一个现有位置,这两者都在我们视图的内部数据中产生模型。
从视图模型的属性中读取数据通常没问题,但写入数据则不然,因为本练习的重点是将逻辑与布局分开。如果我们限制编写视图模型数据,你可以立即找到这两个地方——将locations
视图模型中的属性修改为:
现在我们已经说过读取位置很好,但只有类本身可以写入位置。Xcode 会立即指出我们需要从视图中获取代码的两个位置:添加新位置和更新现有位置。
因此,我们可以从向视图模型添加一个新方法来处理添加新位置开始:
然后可以从中的 + 按钮使用它ContentView
:
第二个有问题的地方是更新一个位置,所以我希望你把整个if let index
检查剪切到剪贴板,然后将它粘贴到视图模型中的一个新方法中,添加一个检查,我们有一个选定的地方可以使用:
确保并viewModel
从那里删除两个引用——它们不再需要了。
现在EditView
工作表ContentView
可以将其数据传递到视图模型:
此时视图模型已经接管了所有方面ContentView
,这很棒:视图在那里呈现数据,视图模型在那里管理数据。分裂并不总是那么干净,尽管你可能会在网上其他地方听到这种说法,这也没关系——一旦你进入更高级的项目,你会发现“一刀切”的方法通常不适合任何人,所以我们尽我们所能。
无论如何,在这种情况下,既然我们已经设置好视图模型,我们可以升级它以支持数据的加载和保存。这将在文档目录中查找特定文件,然后使用JSONEncoder
或JSONDecoder
转换它以备使用。
之前我向你展示了如何使用可重用功能找到我们应用程序的文档目录,但在这里我们将把它打包为一个扩展,FileManager
以便在任何项目中更容易访问。
创建一个名为 FileManager-DocumentsDirectory.swift 的新 Swift 文件,然后为其提供以下代码:
现在我们可以在我们的文档目录中的任何地方创建一个文件的 URL,但是我不想在加载和保存文件时都这样做,因为这意味着如果我们改变了我们的保存位置,我们需要记住更新这两个地方.
因此,更好的想法是向我们的视图模型添加一个新属性来存储我们要保存到的位置:
有了它,我们就可以创建一个新的初始化器和一个新的save()
方法来确保我们的数据被自动保存。首先将其添加到视图模型:
至于保存,之前我向你展示了如何将一个字符串写入磁盘,但是这个Data
版本更好,因为它让我们只用一行代码就可以做一些非常神奇的事情:我们可以要求 iOS 确保文件是加密写入的,所以只有在用户解锁他们的设备后才能读取它。这是请求原子写入的补充——iOS 几乎为我们完成了所有工作。
现在将此方法添加到视图模型:
是的,要确保文件以强加密方式存储,只需添加.completeFileProtection
数据写入选项即可。
使用这种方法,我们可以在任意数量的文件中写入任意数量的数据——它比UserDefaults
灵活得多,还允许我们根据需要加载和保存数据,而不是像UserDefaults
那样在应用程序启动时立即加载和保存数据。
在我们完成这一步之前,我们需要对我们的视图模型进行一些小的更改,以便使用我们刚刚编写的代码。
首先,locations
数组不再需要初始化为空数组,因为这是由初始化程序处理的。将其更改为:
其次,我们需要save()
在添加新位置或更新现有位置后调用该方法,因此请将其添加save()
到这两个方法的末尾。
继续并立即运行该应用程序,你应该会发现你可以自由添加项目,然后重新启动该应用程序以查看它们恢复原样。
这总共花费了相当多的代码,但最终结果是我们已经很好地完成了加载和保存:
所有逻辑都在视图之外处理,因此稍后当你学习编写测试时,你会发现使用视图模型要容易得多。
当我们写入数据时,我们会让 iOS 对其进行加密,以便在用户解锁设备之前无法读取或写入文件。
加载和保存过程几乎是透明的——我们添加了一个修改器并更改了另一个,仅此而已。
当然,我们的应用程序还不是真正安全:我们已经确保我们的数据文件使用加密保存,因此只有在设备解锁后才能读取它,但没有什么可以阻止其他人之后读取数据。



将我们的 UI 锁定在 Face ID 后面
为了完成我们的应用程序,我们将进行最后一项重要更改:我们将要求用户使用 Touch ID 或 Face ID 对自己进行身份验证,以便查看他们在应用程序上标记的所有位置。毕竟,这是他们的私人数据,我们应该尊重这一点,当然,这给了我一个机会,让你在实际环境中使用一项重要技能!
首先,我们需要在我们的视图模型中添加一些新状态来跟踪应用程序是否已解锁。因此,首先添加这个新属性:
其次,我们需要在我们的项目配置选项中添加 Face ID 权限请求密钥,向用户解释我们为什么要使用 Face ID。如果你还没有添加它,现在转到你的目标选项,选择信息选项卡,然后右键单击任何现有行并在其中添加“隐私 - 面部识别码使用说明”键。你可以输入你喜欢的内容,但“请验证你自己以解锁你的位置”似乎是一个不错的选择。
第三,我们需要添加import LocalAuthentication
到你的视图模型文件的顶部,以便我们可以访问 Apple 的身份验证框架。
现在是困难的部分。如果你还记得,由于其 Objective-C 根源,用于生物识别身份验证的代码有点令人不快,因此最好让它远离 SwiftUI 的整洁。因此,我们将编写一个专用authenticate()
方法来处理所有生物识别工作:
创建一个
LAContext
这样我们就有了可以检查和执行生物认证的东西。询问当前设备是否能够进行生物认证。
如果是,则启动请求并提供一个闭包以在它完成时运行。
请求完成后,检查结果。
如果成功,我们将设置
isUnlocked
为 true 以便我们可以正常运行我们的应用程序。
现在将此方法添加到你的视图模型中:
请记住,我们代码中的字符串用于 Touch ID,而 Info.plist 中的字符串用于 Face ID。
现在我们需要做一个实际上非常小的调整,但如果你正在阅读本文而不是观看视频,则可能很难形象化。ZStack
中的所有内容都
需要缩进一层,并将其放在它前面:
在结束之前添加ZStack
:
所以,它应该是这个样子:
所以现在我们需要做的就是用触发authenticate()
方法的实际按钮填写评论// button here
。你可以设计任何你想要的,但这样的东西应该足够了:
你现在可以继续并再次运行该应用程序,因为我们的代码几乎完成了。如果这是你第一次在模拟器中使用 Face ID,你需要转到“功能”菜单并选择“Face ID”>“已注册”,但是一旦你重新启动应用程序,你就可以使用“功能”>“Face ID”>“匹配面部”进行身份验证。
但是,当它运行时,你可能会注意到一个问题:应用程序似乎工作正常,但 Xcode 可能会在其调试输出中显示一条警告消息。更重要的是,它还会显示一个紫色警告,这是 Xcode 标记运行时问题的问题——当我们的代码做了一些它确实不应该做的事情时。
在这种情况下,它应该指向我们视图模型中的这一行:
旁边应该写着“不允许从后台线程发布更改”,这意味着“你正在尝试更改 UI,但你不是从主要参与者那里做的,这会导致问题。”
现在,这可能会令人困惑,因为我们之前专门将@MainActor
属性添加到我们的整个类,我说这意味着该类的所有代码都将在主要参与者上运行,因此对于 UI 更新是安全的。但是,我在那里添加了一个重要的附带条件:“除非我们特别要求。”
在这种情况下,我们确实提出了其他要求,但可能并不明显:当我们要求 Face ID 完成用户身份验证工作时,这发生在我们的程序之外——不是我们在进行实际的面部检查,而是 Apple。当该过程完成时,Apple 将调用我们的完成闭包来说明它是否成功,但不管我们的@MainActor
属性如何,它都不会在主要参与者上调用。
这里的解决方案是确保我们更改主要角色的属性isUnlocked
。这可以通过启动一个新任务,然后在那里调用await MainActor.run()
来完成,如下所示:
这实际上意味着“启动一个新的后台任务,然后立即使用该后台任务对主要参与者的一些工作进行排队。”
这行得通,但我们可以做得更好:我们可以告诉 Swift 我们的任务代码需要直接在主要参与者上运行,方法是给闭包本身一个@MainActor
属性。因此,新任务不会跳转到后台任务然后返回到主要参与者,而是会立即开始在主要参与者上运行:
这样我们的代码就完成了,这是另一个完整的应用程序——干得好!


