Hacking with iOS: SwiftUI Editio
What you learned - 你学到了什么
这最后三个项目确实推动了数据的开发,首先是通过互联网发送和接收数据,然后进入Core Data,以便您可以了解实际应用如何管理其数据。您在此项目中学到的技能也许比您意识到的更为重要,因为如果将它们全部组合在一起,您现在可以从Internet上获取数据,将其存储在本地,并让用户进行过滤以查找他们关心的内容。
以下是我们在最近三个项目中介绍的所有新内容的快速回顾:
- 使自定义类符合
Codable
协议 - 使用
URLSession
发送和接收数据 - 视图的
disabled()
修改器 - 使用
@Bindable
构建自定义UI组件 - 使用
AnyView
进行类型擦除 - 向
Alert
警报添加多个按钮 - SwiftUI中如何使用Swift的
Hashable
协议 - 使用
@FetchRequest
属性包装器查询 Core Data - 使用
NSSortDescriptor
对 Core Data 查询结果进行排序 - 创建自定义
NSManagedObject
子类 - 使用
NSPredicate
过滤数据
与其他项目相比,这是一个相对较短的列表,但是我认为可以说这些主题确实是一个真正的进步:Core Data 在某些地方很难,尤其是我们需要如何桥接NSSet
之类的东西,以便它们在充满光明前景的SwiftUI环境中表现出色和拥有美好的未来。
Key points - 关键点
尽管我们在后三个项目中介绍了很多内容,但是我还是要特别详细介绍两件事:类型擦除和Codable
。我们已经在我们的项目中对这些内容进行了一些研究,但是您还应该多花些时间……
AnyView vs Group:实际应用中的类型擦除
SwiftUI的视图只有一个要求,那就是它们具有返回某些特定类型视图的body
属性。正如我们在较早的技术项目中所看到的那样,指定精确的返回类型很麻烦,因为在应用修饰符时SwiftUI会构建容器,这就是为什么我们拥有some View
的原因:“这将返回一种特定的视图,但是我们不想说他到底是什么。”
但是,这样做有一个缺点:我们无法动态确定返回的视图类型。这意味着我们有时无法返回文本视图,而有时无法返回图像,但是由于SwiftUI使用修饰符容器包装视图的方式,甚至意味着我们不能混合和匹配许多修饰符。例如,这种代码无效:
struct ContentView: View {
var body: some View {
if Bool.random() {
return Text("Hello, World!")
.frame(width: 300)
} else {
return Text("Hello, World!")
}
}
}
解决此问题的一种方法是使用类型擦除,这是隐藏某些数据的基础类型的过程。这在Swift中很常用:我们有类型擦除包装器,例如AnyHashable
和AnySequence
,它们所做的只是充当外壳程序,将其操作转发到它们所包含的内容,而不会向外部揭示内容。
在SwiftUI中,我们为此目的提供了AnyView
:它可以在其中容纳任何类型的视图,这使我们可以自由地混合和匹配视图,如下所示:
struct ContentView: View {
var body: some View {
if Bool.random() {
return AnyView(Text("Hello, World!")
.frame(width: 300))
} else {
return AnyView(Text("Hello, World!"))
}
}
}
但是,使用AnyView
会带来性能损耗:通过隐藏视图的结构方式,当视图层次结构发生更改时,我们将迫使SwiftUI进行更多工作——如果我们在其中一种擦除类型中对SwiftUI进行了少量更改我们的视图层次结构的一部分,很有可能需要重新创建整个事物。
这里有一个替代方法,尽管它并不是AnyView
提供的所有功能的真正替代方法,但仍然值得花费很多时间。替代方法是使用像Group
这样的容器:
struct ContentView: View {
var body: some View {
Group {
if Bool.random() {
Text("Hello, World!")
.frame(width: 300)
} else {
Text("Hello, World!")
}
}
}
}
即使具有返回文本视图或修改后的文本视图的条件,它们都被包装在一个组中,因此可以满足some View
的要求。
当然,应该出现的问题是:为什么不在各个地方都使用Group
?从理论上讲——从理论上讲,使用Group
应该总是更快,因为它不会从SwiftUI中隐藏信息,这又意味着如果您定期更改视图层次结构,它可以避免做额外的工作。
实际上,在我看来,这就像过早优化的一种情况:如果使用AnyView
时遇到性能问题,我会感到惊讶,并且如果这样做的话,您可以迁移而不是事先计划。实际上,您的代码可以做的最重要的事情是传达您的意图,对我来说,组用于:
- 突破10个子视图的限制:每个组可以拥有自己的10个子视图,因此您可以在组内创建组以创建更复杂的布局
- 将布局委托给父容器。如果您创建的自定义视图的主体是一个顶层的组,则可以将该视图嵌入到
HStack
或VStack
中以动态更改其布局。
- 将布局委托给父容器。如果您创建的自定义视图的主体是一个顶层的组,则可以将该视图嵌入到
- 让我们一次将一组修饰符应用于多个视图。
另一方面,AnyView
专门用于类型擦除,因此,当您看到它在运行时,您会立即知道它存在原因。
有时候,Group
根本不会削减它,因为它没有AnyView
的类型擦除功能。例如,您不能创建一组数组,因为[Group]
本身没有意义——SwiftUI想要知道该组中的内容。另一方面,[AnyView]
很好,因为AnyView
的意义在于内容无关紧要。
因此,只有通过实际的类型擦除才能实现这种代码:
struct ContentView: View {
@State var views = [AnyView]()
var body: some View {
VStack {
Button("Add Shape") {
if Bool.random() {
self.views.append(AnyView(Circle().frame(height: 50)))
} else {
self.views.append(AnyView(Rectangle().frame(width: 50)))
}
}
ForEach(0..<views.count, id: \.self) {
self.views[$0]
}
Spacer()
}
}
}
每次您点击按钮时,都会将一个形状添加到数组中,但是由于[Shape]
和[Group]
都没有意义,因此必须设置为[AnyView]
。
如果您打算经常使用类型擦除,则值得添加此便捷扩展:
extension View {
func erasedToAnyView() -> AnyView {
AnyView(self)
}
}
通过这种方法,我们可以将erasedToAnyView()
视为修饰符:
Text("Hello World")
.font(.title)
.erasedToAnyView()
随着SwiftUI的不断发展,我希望我们能更清楚地了解什么时候Group
相对于AnyView
而言具有更大的性能提升,但是就我的喜好而言,现在感觉有点像货物崇拜编程。
Codable keys
当我们拥有与设计类型相匹配的JSON数据时,Codable
可以完美地工作。实际上,如果我们不使用@Published
之类的属性包装器,那么除了添加Codable
协议遵守外,我们通常不需要执行任何其他操作——Swift编译器会自动合成我们需要的所有内容。
但是,很多时候事情并不是那么简单。在这种情况下,我们可能需要编写自定义的Codable
符合性——即,手动编写init(from :)
和encode(to :)
——但存在中间立场,在某些指导下,Codable
仍可以完成我们的大部分工作。
一个常见的例子是传入的JSON对属性使用不同的命名约定。例如,我们可能会以蛇形(例如first_name
)接收JSON属性名称,而我们的Swift代码会以驼峰式(例如firstName
)使用属性名称。只要知道要期待的东西,Codable
就可以在这两者之间进行翻译——我们需要在解码器上设置一个名为keyDecodingStrategy
的属性。
为了说明这一点,这是一个具有两个属性的User
结构体:
struct User: Codable {
var firstName: String
var lastName: String
}
这是一些具有相同两个属性的JSON数据,但是使用了蛇形大小写:
let str = """
{
"first_name": "Andrew",
"last_name": "Glouberman"
}
"""
let data = Data(str.utf8)
如果我们尝试将该JSON解码为User
实例,它将无法正常工作:
do {
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
print("Hi, I'm \(user.firstName) \(user.lastName)")
} catch {
print("Whoops: \(error.localizedDescription)")
}
但是,如果在调用decode()
之前修改密钥解码策略,则可以要求Swift将蛇形大小写转换为驼峰大小写。因此,这将成功:
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let user = try decoder.decode(User.self, from: data)
print("Hi, I'm \(user.firstName) \(user.lastName)")
} catch {
print("Whoops: \(error.localizedDescription)")
}
当我们在与camelCase之间来回转换snake_case时,效果很好,但是如果我们的数据完全不同怎么办?
作为示例,请看以下JSON:
let str = """
{
"first": "Andrew",
"last": "Glouberman"
}
"""
它仍然具有用户的名字和姓氏,但是属性名称根本与我们的结构不符。
当我们查看Codable
时,我说过我们可以创建一个编码键枚举,以描述应该对哪些键进行编码和解码。当时我说:“这个枚举通常称为CodingKeys
,最后加一个S,但如果需要,也可以将其命名为其他名称。”尽管如此,但这并不是全部。
您会看到,我们通常使用CodingKeys
作为名称的原因是该名称具有超能力:如果存在CodingKeys
枚举,Swift将在我们不提供自定义Codable
实现的时候自动使用它来决定如何对对象进行编码和解码。
我意识到要花费很多时间和精力来理解,所以最好用一些代码来证明。尝试将User
结构体更改为此:
struct User: Codable {
enum ZZZCodingKeys: CodingKey {
case firstName
}
var firstName: String
var lastName: String
}
该代码可以很好地编译,因为名称ZZZCodingKeys
对Swift毫无意义——它只是一个嵌套枚举。但是,如果将枚举重命名为CodingKeys
,您将发现不再构建代码:我们现在指示Swift仅对firstName
属性进行编码和解码,这意味着没有用于设置lastName
属性设置的初始化程序——但这不是允许的。
所有这些都很重要,因为CodingKeys
具有第二种超级功能:如果将原始值字符串附加到属性,Swift将使用这些字符串作为JSON属性名称。也就是说,案例名称应与我们的Swift属性名称匹配,并且案例值应与JSON属性名称匹配。
因此,让我们回到示例JSON:
let str = """
{
"first": "Andrew",
"last": "Glouberman"
}
"""
该属性名称使用“ first”和“ last”,而我们的User
结构体使用firstName
和lastName
。这是可以很好地利用CodingKeys
的好地方:我们不需要编写自定义的Codable
一致性,因为我们只需添加将Swift属性名称与JSON属性名称结合起来的编码密钥即可,如下所示:
struct User: Codable {
enum CodingKeys: String, CodingKey {
case firstName = "first"
case lastName = "last"
}
var firstName: String
var lastName: String
}
现在我们已经专门告诉Swift如何在JSON和Swift命名之间进行转换,我们不再需要使用keyDecodingStrategy
——仅添加枚举就足够了。
因此,尽管您确实需要了解如何创建自定义的Codable
一致性,但是,如果有其他选择,通常最好不要这样做。
Challenge - 挑战
现在是时候从头开始构建应用程序了,今天这是一个特别艰巨的挑战:您的工作是使用URLSession
从互联网上下载一些JSON,使用Codable
将其转换为Swift类型,然后使用NavigationView
,List
等显示给用户。
您的第一步应该是检查JSON。您要使用的URL是:https://www.hackingwithswift.com/samples/friendface.json——这是用户随机生成的大量数据的集合的一个示例。
如您所见,这里有许多人,每个人都有一个ID,姓名,年龄,电子邮件地址等等。它们还具有一个标签字符串数组和一个朋友数组,其中每个朋友都有一个名称和ID。
具体实现的程度取决于您,但是至少您应该:
- 获取数据并将其解析为
User
和Friend
结构体。 - 显示用户列表以及有关他们的一些信息。
- 创建一个细部视图,当用户被点击时显示,显示有关他们的更多信息。
事情变得更有趣的是与他们的朋友在一起:如果您真的想提高自己的技能,请考虑如何在详细信息屏幕上向每个用户的朋友展示。
对于中型挑战,请在详细信息屏幕上显示一些有关朋友的信息。如果遇到更大的挑战,请使每个好友都可以轻按以显示自己的详细视图。
即使有很多数据,我们一次只与100个朋友一起工作——使用first(where :)
之类的方法在数组中查找朋友是完全可以的。
如果您不确定从何处开始,请先设计类型:首先构建具有名称,年龄,公司等属性的User
结构体,然后构建具有id
和name
的Friend
结构体。之后,添加一些URLSession
代码以获取数据并将其解码为您的类型。
在构建此应用程序时,我希望您牢记一件事:此类应用程序是iOS应用程序开发的生死攸关的东西——如果您可以放心地将其完成,那么您将迈向全面发展担任应用程序开发人员的时期。