SwiftUI学习100天(Day60 - 里程碑:项目 10-12)

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

这是完成的另外三个项目,以及更多真正重要的技术。无论你的设计多么漂亮或你的应用创意多么聪明,处理好用户数据几乎始终是任何优秀应用最重要的事情。
当然,真正讨论的是“好”是什么意思。至少我希望这意味着“尊重”——未经他们的同意,你不会分享任何东西,你不会在未经许可的情况下跟踪他们的活动,并且你会小心地存储任何个人数据。除此之外,你可能想要添加搜索或过滤,你可能想要云同步以便他们的数据在设备之间共享,你可能想要让他们浏览或修改原始数据,等等。
无论你用它做什么,学习使用用户数据都是一项很好的技能,并且你在这最后三个项目中已经向前迈进了很多步。
现在是挑战的时候了,毫不奇怪,它将涉及获取、处理和显示大量数据。你已经具备了使这款应用程序成为出色应用程序所需的所有技能,所以剩下的就是让你打开一个新的 Xcode 项目并投入其中。
你会犯错吗?是的——没关系。英国作家尼尔·盖曼 (Neil Gaiman) 提出了一些建议,希望对你有所帮助:
我希望在接下来的一年里,你会犯错误。因为如果你在犯错误,那么你就是在创造新事物、尝试新事物、学习、生活、推动自己、改变自己、改变你的世界。你正在做你以前从未做过的事情,更重要的是,你正在做一些事情。
今天你有三个主题要完成,其中之一是你的挑战。

你学到了什么
这最后三个项目确实在数据上施加了很大的压力,首先是使用互联网发送和接收数据,然后进入核心数据,这样你就可以看到真正的应用程序是如何管理它们的数据的。你在这个项目中学到的技能可能比你意识到的更重要,因为如果你把它们放在一起,你现在可以从 Internet 获取数据,将其存储在本地,并让用户过滤以找到他们关心的东西。
以下是我们在过去三个项目中涵盖的所有新事物的快速回顾:
构建自定义
Codable
一致性使用
URLSession
发送和接收数据
disabled()
视图修饰符使用构建自定义 UI 组件
@Bindable
向警报alert弹窗添加多个按钮
SwiftUI中如何使用Swift的Hashable
协议使用
@FetchRequest
属性包装器查询核心数据使用
SortDescriptor
排序核心数据结果
创建自定义
NSManagedObject
子类使用过滤数据
NSPredicate
创建核心数据实体之间的关系
与其他一些项目相比,这是一个相对较短的列表,但我认为可以公平地说这些主题是一个真正的进步:核心数据在某些地方很难,特别是我们需要如何桥接NSSet
诸如此类的东西
以便它们在光明和SwiftUI 的未来。

关键点
尽管我们在最后三个项目中涵盖了很多内容,但我特别想更详细地介绍两件事:类型擦除和Codable
. 我们已经在我们的项目中对这些进行了一些研究,但正如你将看到的那样,它们还需要一些额外的时间……
返回不同的视图
SwiftUI 的视图只有一个要求,即它们具有body
返回某种特定类型视图的属性。正如我们在早期的技术项目中看到的那样,指定精确的返回类型是痛苦的,因为 SwiftUI 在我们应用修饰符时构建容器的方式,这就是为什么我们有some View
– “这将返回一种特定类型的视图,但我们不会想说什么。”
然而,Swift 做了一些非常聪明的事情来帮助我们的生活更轻松:它自动将@ViewBuilder
属性添加到所有 SwiftUI 视图的body
属性中。这只发生在body
上,
而不是任何返回some View
的属性上,但它无声地增加了一个重要的超能力:我们可以返回多个视图,并且也可以根据条件返回不同的视图。
但是,由于其他属性默认情况下无法访问此超级功能:它们有时无法返回文本视图,有时无法返回图像,但由于 SwiftUI 使用修饰符容器包装视图的方式,这甚至意味着它们无法混合并匹配许多修饰符。
所以,这样的代码是无效的:
(注意:出于演示目的,我已将代码放入randomText
中
;显然,在实际项目中,你要么在视图的其他地方有更多代码,要么将其直接放入body
.)
我们有多种方法可以解决这个问题:
我们可以将我们的视图包装在另一个名为
AnyView
的视图中
,它使用类型擦除来隐藏其内容。我们可以手动将
@ViewBuilder
注释添加到其他属性。我们可以将代码包装在一个
Group
中的属性内
,这意味着只返回一个视图。使用三元条件运算符提供不同的值。
我想逐一介绍这些选项,以便你了解它们的外观以及它们的比较方式。
类型擦除是隐藏某些数据的底层类型的过程。这在 Swift 中经常使用:我们有类型擦除包装器,例如AnyHashable
和
AnySequence
,它们所做的只是充当外壳,将它们的操作转发到它们包含的任何内容,而不会向任何外部透露内容。
在 SwiftUI 中我们有AnyView
这个目的:它可以在其中容纳任何类型的视图,这允许我们自由地混合和匹配视图,如下所示:
然而,使用 AnyView
有性能成本
:通过隐藏我们的视图结构方式,我们迫使 SwiftUI 在我们的视图层次结构发生变化时做更多的工作——如果我们在类型擦除的部分之一中进行一个小的更改我们的视图层次结构,很有可能都需要重新创建。
就像我说的,有其他选择,其中之一是在需要的@ViewBuilder
任何地方手动添加属性,如下所示:
有了它,我们不再需要AnyView
或return
关键字,因为它全部由 @ViewBuilder 处理——我们已经使randomText
属性像body
.
有很多地方使用@ViewBuilder
是一个很好的解决方案,包括需要返回非常不同类型的视图的地方。但是,请不要滥用它:如果你发现自己需要经常使用@ViewBuilder
,这可能意味着你试图将太多逻辑塞进你的 SwiftUI 视图中,并且很有可能你会通过将它们分割成更小的视图来做得更好.
另一种选择是使用Grouper
而不是
@ViewBuilder
,如下所示:
老实说,这与使用@ViewBuilder
没什么不同
. 是的,在幕后有一个额外的 SwiftUI 视图在起作用(Group
视图),但它不会对布局产生影响,而且那个额外的视图可能只是被优化掉了。
最后一个选项并不总是可行的,但在这里是可行的,你应该始终注意它:在你可以使用它的地方,三元条件运算符始终是比上述任何解决方案更好的选择。
SwiftUI 的许多修饰符(包括frame()
)允许我们传递nil
而不是值,这会导致修饰符不执行任何操作。其他一些修饰符,如blur()
和opacity()
不允许nil
被传递,但你也可以提供不执行任何操作的默认值 – 例如,blur(radius: 0)
和
opacity(1)
这些被称为惰性修饰符,SwiftUI 只会在布局结果之前删除它们。
所以,我们代码的理想版本是这样的:
为什么是“理想”?因为在这种情况下 SwiftUI 只创建一个文本视图。是的,宽度在固定的 300 点和文本的默认大小之间变化,但创建的实际渲染文本没有变化。相比之下,使用AnyView
、@ViewBuilder
和Group
所有涉及两个视图:当Bool.random()
值从一个状态翻转到另一个状态时,第一个文本视图将被销毁并创建一个新视图。
至少有一次你真的需要AnyView
的类型擦除能力
,但它真的应该是你最后的选择。例如,你不能创建一个组数组,因为[Group]
它本身没有任何意义——SwiftUI 想知道数组中有什么。另一方面,[AnyView]
完全没问题,因为AnyView
内容无关紧要。
所以,这种代码只有在实际类型擦除时才有可能:
每次点击按钮时,都会将一个形状添加到数组中,但由于Shape
和Group
都没有意义,因此必须键入数组[AnyView]
。
如果你打算定期使用类型擦除,那么添加这个方便的扩展是值得的:
通过这种方法,我们可以把erasedToAnyView()像修饰符
一样对待:
作为非常粗略的指导,你应该:
旨在使用三元条件运算符而不是使用
if
条件。宁愿将大视图分解成更小的视图,也不要在视图层次结构中添加复杂的逻辑。
使用Group
避免 10个视图的限制,或在其他情况下不可能的地方添加修饰符,例如navigationTitle()
。仅对简单的属性使用显式
@ViewBuilder
,但当新视图可能更有意义时,请谨慎使用它来掩盖复杂的逻辑。如果其他选项都不起作用,则退而求其次使用
AnyView
。
编码键
当我们拥有与我们设计类型的方式相匹配的 JSON 数据时,就Codable
可以完美地工作。事实上,如果我们不使用诸如 @Published
之类的属性包装器
,我们通常不需要做任何事情,除了添加Codable
一致性之外——Swift 编译器会自动合成我们需要的一切。
然而,很多时候事情并不是那么简单。在这些情况下,我们可能需要编写自定义Codable
一致性——即,手写init(from:)
——encode(to:)
但有一个中间地带,在一些指导下,Codable
仍然可以为我们完成大部分工作。
一个常见的例子是我们传入的 JSON 对其属性使用不同的命名约定。例如,我们可能会收到蛇形大小写的 JSON 属性名称(例如first_name
),而我们的 Swift 代码使用驼峰大小写的属性名称(例如firstName
)。Codable
只要它知道期望什么,它就能够在这两者之间进行转换——我们需要在我们的解码器上设置一个名为keyDecodingStrategy
.
为了证明这一点,这里有一个User
具有两个属性的结构:
下面是一些具有相同两个属性的 JSON 数据,但使用蛇形大小写:
如果我们尝试将该 JSON 解码为一个User
实例,它将不起作用:
但是,如果我们在调用之前修改密钥解码策略decode()
,我们可以要求 Swift 将蛇形大小写与驼峰大小写相互转换。所以,这会成功:
当我们将 snake_case 与 camelCase 相互转换时效果很好,但如果我们的数据完全不同怎么办?
例如,看一下这个 JSON:
它仍然有用户的名字和姓氏,但属性名称与我们的结构完全不匹配。
当我们查看Codable
时,
我说过我们可以创建一个编码密钥枚举,描述哪些密钥应该被编码和解码。当时我说“这个枚举通常被称为CodingKeys
,末尾有一个 S,但如果你愿意,你可以称它为其他东西”,虽然这是真的,但这不是全部。
你看,我们通常使用CodingKeys
名称的原因是这个名称具有超能力:如果CodingKeys
存在枚举,Swift 将自动使用它来决定如何在我们不提供自定义Codable
实现的情况下对对象进行编码和解码。
我意识到要接受的内容很多,所以最好用一些代码来演示。尝试将User
结构更改为此:
该代码将编译得很好,因为名称ZZZCodingKeys
对 Swift 没有意义——它只是一个嵌套的枚举。但是,如果你将枚举重命名为CodingKeys,
你会发现代码不再构建:我们现在指示 Swift 只对firstName
属性进行编码和解码,这意味着没有初始化器来处理设置lastName
属性——这是不允许的。
所有这些都很重要,因为CodingKeys
它有第二个超能力:如果我们将原始值字符串附加到我们的属性,Swift 将使用它们作为 JSON 属性名称。也就是说,case 名称应该与我们的 Swift 属性名称相匹配,case值应该与 JSON 属性名称相匹配。
那么,让我们回到我们的示例 JSON:
它使用“first”和“last”作为属性名称,而我们的User
结构体使用的是firstName
和
lastName
。这是CodingKeys
可以解决问题的一个好地方
:我们不需要编写自定义Codable
一致性,因为我们只需添加将 Swift 属性名称与 JSON 属性名称结合起来的编码键,如下所示:
既然我们已经明确告诉 Swift 如何在 JSON 和 Swift 命名之间转换,我们就不再需要使用了keyDecodingStrategy
——只需要添加那个枚举就足够了。
因此,虽然你确实需要知道如何创建自定义Codable
一致性,但如果这些其他选项可行,通常最好不要使用它。



挑战
现在是你从头开始构建应用程序的时候了,今天这是一个特别广泛的挑战:你的工作是使用URLSession
从互联网上下载一些 JSON,使用Codable
将其转换为 Swift 类型,然后使用NavigationView
、List
等将其显示到用户。
你的第一步应该是检查 JSON。你使用的 URL 是:https ://www.hackingwithswift.com/samples/friendface.json – 这是大量随机生成的用户数据集合。
如你所见,有一个人数组,每个人都有一个 ID、姓名、年龄、电子邮件地址等。他们还有一组标签字符串和一组朋友,其中每个朋友都有一个名字和 ID。
你实现它的程度取决于你,但至少你应该:
获取数据并将其解析为
User
和Friend
结构。显示一个用户列表,其中包含一些关于他们的信息,例如他们的姓名以及他们现在是否活跃。
创建点击用户时显示的详细信息视图,显示有关他们的更多信息,包括他们朋友的名字。
在开始下载之前,请检查你的
User
数组是否为空,这样你就不会在每次显示视图时都继续开始下载。
如果你不确定从哪里开始,请先设计你的类型:构建一个具有name
、
age
、
company
等属性的
User
结构体,然后构建一个具有id
和
name
的
Friend
结构。之后,转到一些
URLSession
代码来获取数据并将其解码为你的类型。
你可能会注意到每个用户注册的日期都有一个非常特定的格式:2015-11-10T01:47:18-00:00。这被称为 ISO-8601,它是如此普遍以至于有一个内置的dateDecodingStrategy
调用.iso8601
可以自动解码它。
在你构建此应用程序时,我希望你牢记一件事:此类应用程序是 iOS 应用程序开发的基础——如果你能自信地搞定这一点,那么你就已经走上了全面发展的道路——作为应用程序开发人员的时间工作。
提示:一如既往,解决这一挑战的最佳方法是保持简单——编写尽可能少的代码来解决挑战,并且让你对它运行良好感到满意。


