SwiftUI学习100天(Day68 - 项目 14,第一部分)

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

你会很高兴知道今天是你一段时间以来最轻松的一天。这并不意味着我们正在研究不重要的东西,只是我们所涵盖的新技术几乎可以保证是你最近不得不面对的协调器、选择器和更多东西的可喜突破。
我所能做的就是:在它持续的时候享受它!明天我们将回到困难的部分,鉴于你在这 100 天内已经超过三分之二大关,我希望这不会让人感到意外。
继续前进!正如文斯·隆巴尔迪 (Vince Lombardi) 所说,“字典里只有成功先于工作的地方。”
今天你有四个主题要完成,你将在其中学习如何实现Comparable
、查找用户的文档目录等。

愿望清单:简介
在这个项目中,我们将构建一个应用程序,让用户可以在地图上构建他们打算某天访问的地点的私人列表,为该地点添加描述,查找附近有趣的地点,并保存它全部到 iOS 存储以备后用。
要使所有这些工作正常进行,将意味着利用你已经掌握的一些技能,例如表单、工作表、Codable
和URLSession
,但也会教给你一些新技能:如何在 SwiftUI 应用程序中嵌入地图,如何安全地存储私有数据,以便只有经过身份验证的用户才能访问它,如何在 UserDefaults
之外加载和保存数据
,等等。
因此,有很多东西要学习,还有另一个很棒的应用程序要制作!不管怎样,让我们开始使用我们的技术:使用 App 模板创建一个新的 iOS 项目,并将其命名为 BucketList。现在开始我们的技术……



为自定义类型添加对 Comparable 的一致性
仔细想想,当我们编写 Swift 代码时,我们认为很多东西都是理所当然的。例如,如果我们写4 < 5
,我们希望它返回真——Swift(和 LLVM,Swift 背后更大的编译器项目)的开发者已经完成了检查该计算是否真实的所有艰苦工作,所以我们不不必担心。
但 Swift真正擅长的是使用协议和协议扩展将功能扩展到很多地方。例如,我们知道这4 < 5
是真的,因为我们能够比较两个整数并确定第一个整数是在第二个整数之前还是之后。Swift 将该功能扩展到整数数组:我们可以比较数组中的所有整数来决定每个整数应该在其他整数之前还是之后。Swift 然后使用该结果对数组进行排序。
所以,在 Swift 中,我们希望这种代码能够正常工作:
我们不需要告诉sorted()
它应该如何工作,因为它了解整数数组的工作方式。
现在考虑这样一个结构体:
我们可以将这些用户组成一个数组,并在List
这样的内部使用它们
:
那会工作得很好,因为我们使User
结构符合Identifiable
.
但是如果我们想按排序顺序显示这些用户呢?如果我们将代码修改成这样,它将不起作用:
Swift 不明白sorted()
在
这里的意思,因为它不知道是按名字、姓氏、两者还是其他方式排序。
之前我向你展示了我们如何提供一个闭包来让sorted()
自己进行排序,我们可以在这里使用相同的方法:
这绝对有效,但由于两个原因,它不是理想的解决方案。
首先,这是模型数据,我的意思是它会影响我们使用User
结构的方式。该结构及其属性是我们的数据模型,在一个开发良好的应用程序中,我们真的不想告诉模型它在我们的 SwiftUI 代码中应该如何表现。SwiftUI 代表我们的视图,即我们的布局,如果我们将模型代码放在那里,事情就会变得混乱。
其次,如果我们想User
在多个地方对数组进行排序会怎样?你可能会复制并粘贴一次或两次闭包,然后才意识到你只是在给自己制造一个问题:如果你最终更改了排序逻辑以便firstName
在姓氏相同时也使用,那么你需要搜索你所有的代码以确保所有闭包都得到更新。
Swift 有更好的解决方案。整数数组有一个没有参数的简单sorted()
方法,因为 Swift 知道如何比较两个整数。在编码术语中,Int
符合Comparable
协议,这意味着它定义了一个函数,该函数接受两个整数并在第一个应该排在第二个之前返回 true。
我们可以让我们自己的类型符合Comparable
,当我们这样做时,我们也会得到一个sorted()
没有参数的方法。这需要两个步骤:
将
Comparable
一致性添加到User
的定义中
。添加一个名为的方法,该方法
<
需要两个用户,如果第一个用户应该排在第二个用户之前,则返回 true。
这是代码中的样子:
里面的代码不多,但是还是有很多要展开的。
首先,是的,该方法只是被调用<
,这是“小于”运算符。确定一个用户是否“小于”(在排序意义上)另一个用户是该方法的工作,因此我们正在向现有运算符添加功能。这称为运算符重载,它既是福也是祸。
其次,lhs
和rhs
是“左手边”和“右手边”的缩写,使用它们是因为<
运算符在其左侧和右侧有一个操作数。
第三,这个方法必须返回一个布尔值,这意味着我们必须决定一个对象是否应该排在另一个对象之前。这里没有“它们相同”的余地——这是由另一个名为Equatable
.
第四,该方法必须标记为static
,这意味着它是User
直接在结构上调用的,而不是结构的单个实例。
最后,我们的逻辑非常简单:我们只是将比较传递给我们的一个属性,要求 Swift 将其<
用于两个姓氏字符串。你可以根据需要添加任意多的逻辑,根据需要比较任意多的属性,但最终你需要返回 true 或 false。
提示:你在该代码中看不到的一件事是,符合Comparable
也使我们能够访问>
运算符 – 大于。这与 <
相反,因此 Swift 通过使用布尔值并在 true 和 false 之间翻转
<
来为我们创建它。
现在我们的User
结构符合Comparable
,我们自动获得了无参数版本的访问权限sorted()
,这意味着这种代码现在可以工作了:
这解决了我们之前遇到的问题:我们现在将我们的模型功能隔离在结构本身中,我们不再需要复制和粘贴代码——我们可以sorted()
在任何地方使用,安全地知道如果我们改变算法那么我们所有的代码会适应。



将数据写入文档目录
之前我们研究了如何读取和写入数据UserDefaults
,这对于用户设置或少量 JSON 非常有用。但是,它通常不是存储数据的好地方,尤其是当你认为将来会开始存储更多数据时。
在此应用程序中,我们将允许用户创建他们想要的尽可能多的数据,这意味着我们需要一个更好的存储解决方案,而不是仅仅将东西扔进去UserDefaults
并希望最好的。幸运的是,iOS 使得从设备存储中读取和写入数据变得非常容易,事实上所有应用程序都有一个目录来存储我们想要的任何类型的文档。这里的文件自动与 iCloud 备份同步,因此如果用户获得新设备,我们的数据将与所有其他系统数据一起恢复——我们甚至不需要考虑它。
有一个陷阱——不是总是有吗?– 而且所有 iOS 应用程序都是沙盒化的,这意味着它们在自己的容器中运行,目录名称难以猜测。因此,我们不能——也不应该尝试——猜测我们应用程序的安装目录,而是需要依赖 Apple 的 API 来查找我们应用程序的文档目录。
没有什么好的方法可以做到这一点,所以我几乎总是将相同的辅助方法复制并粘贴到我的项目中,我们现在将做完全相同的事情。这使用了一个名为 FileManager
的新类
,它可以为我们提供当前用户的文档目录。理论上这可以返回多个路径 URL,但我们只关心第一个。
因此,将此方法添加到ContentView
:
该文档目录是我们的,可以随心所欲地处理,因为它属于应用程序,所以如果应用程序本身被删除,它会自动被删除。除了物理设备限制外,我们可以存储多少没有限制,但请记住,用户可以使用“设置”应用程序查看你的应用程序占用了多少存储空间——请尊重!
现在我们有了一个可以使用的目录,我们可以在那里自由地读写文件。你已经遇到了String(contentsOf:)
和Data(contentsOf:)
用于
读取数据的方法,但是对于写入数据我们需要使用该write(to:)
方法。当与字符串一起使用时,这需要三个参数:
写给一个
URL
。是否使写入成为原子的,这意味着“一次全部”。
使用什么字符编码。
第一个可以通过将文档目录 URL 与文件名组合来创建,例如 myfile.txt。
第二个应该几乎总是设置为真。如果将其设置为 false 并且我们尝试写入一个大文件,则我们应用程序的另一部分可能会在文件仍在写入时尝试读取该文件。这不应该导致崩溃或任何事情,但它确实意味着它将只读取部分数据,因为另一部分尚未写入。原子写入导致系统将我们的完整文件写入一个临时文件名(不是我们要求的那个),完成后它会简单地重命名为我们的目标文件名。这意味着要么整个文件都存在,要么什么都没有。
第三个参数是我们在项目 5 中简要查看的内容,因为我们必须使用带有 Objective-C API 的 Swift 字符串。那时我们使用字符编码 UTF-16,这是 Objective-C 使用的,但 Swift 的本机编码是 UTF-8,所以我们将改用它。
为了将所有这些代码付诸实践,我们将修改模板的默认文本视图,以便将测试字符串写入文档目录中的文件,将其读回新字符串,然后将其打印出来——完整的数据读写周期。
将
ContentView
的
为此:body
属性更改
当它运行时,你应该能够点击标签以查看打印到 Xcode 的调试输出区域的“测试消息”。
在我们继续之前,这里有一个小挑战:回到项目 8,我们研究了如何创建一个通用扩展Bundle
,让我们从我们的应用程序包中查找、加载和解码任何Codable
数据。你能否为文档目录编写类似的内容,也许使其成为 FileManager
的扩展名
?



使用枚举切换视图状态
你已经了解了我们如何使用常规 Swift 条件来呈现一种或另一种类型的视图,并且我们查看了如下代码:
提示:返回不同类型的视图时,请确保你在body
属性内部或使用类似@ViewBuilder
或Group
的内容
。
条件视图特别有用的地方是当我们想要显示几种不同状态之一时,如果我们正确地计划它,我们可以保持我们的视图代码小并且易于维护——这是开始训练你的大脑思考 SwiftUI 的好方法建筑学。
这个解决方案有两个部分。第一个是为你要表示的各种视图状态定义一个枚举。例如,你可以将其定义为嵌套枚举:
接下来,为这些状态创建单独的视图。我将在这里使用简单的文本视图,但它们可以容纳任何东西:
如果你愿意,这些视图可以嵌套,但它们不一定是 - 这实际上取决于你是否打算在其他地方使用它们以及你的应用程序的大小。
有了这两个部分,我们现在可以有效地将ContentView
用作跟踪当前应用程序状态并显示相关子视图的简单包装器。这意味着给它一个属性来存储当前LoadingState
值:
然后body中添加代码,
根据枚举的值显示正确的视图,如下所示:
使用这种方法,我们的ContentView
不会随着越来越多的代码被添加到视图中而失控,事实上,我们甚至不知道加载、成功或失败是什么样子。


