Hacking with iOS: SwiftUI Edition - 里程碑:项目 10 - 12

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中很常用:我们有类型擦除包装器,例如AnyHashableAnySequence,它们所做的只是充当外壳程序,将其操作转发到它们所包含的内容,而不会向外部揭示内容。

在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时遇到性能问题,我会感到惊讶,并且如果这样做的话,您可以迁移而不是事先计划。实际上,您的代码可以做的最重要的事情是传达您的意图,对我来说,组用于:

    1. 突破10个子视图的限制:每个组可以拥有自己的10个子视图,因此您可以在组内创建组以创建更复杂的布局
    1. 将布局委托给父容器。如果您创建的自定义视图的主体是一个顶层的组,则可以将该视图嵌入到HStackVStack中以动态更改其布局。
    1. 让我们一次将一组修饰符应用于多个视图。

另一方面,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..

每次您点击按钮时,都会将一个形状添加到数组中,但是由于[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结构体使用firstNamelastName。这是可以很好地利用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类型,然后使用NavigationViewList等显示给用户。

您的第一步应该是检查JSON。您要使用的URL是:https://www.hackingwithswift.com/samples/friendface.json——这是用户随机生成的大量数据的集合的一个示例。

如您所见,这里有许多人,每个人都有一个ID,姓名,年龄,电子邮件地址等等。它们还具有一个标签字符串数组和一个朋友数组,其中每个朋友都有一个名称和ID。

具体实现的程度取决于您,但是至少您应该:

  • 获取数据并将其解析为UserFriend结构体。
  • 显示用户列表以及有关他们的一些信息。
  • 创建一个细部视图,当用户被点击时显示,显示有关他们的更多信息。

事情变得更有趣的是与他们的朋友在一起:如果您真的想提高自己的技能,请考虑如何在详细信息屏幕上向每个用户的朋友展示。

对于中型挑战,请在详细信息屏幕上显示一些有关朋友的信息。如果遇到更大的挑战,请使每个好友都可以轻按以显示自己的详细视图。

即使有很多数据,我们一次只与100个朋友一起工作——使用first(where :)之类的方法在数组中查找朋友是完全可以的。

如果您不确定从何处开始,请先设计类型:首先构建具有名称,年龄,公司等属性的User结构体,然后构建具有idnameFriend结构体。之后,添加一些URLSession代码以获取数据并将其解码为您的类型。

在构建此应用程序时,我希望您牢记一件事:此类应用程序是iOS应用程序开发的生死攸关的东西——如果您可以放心地将其完成,那么您将迈向全面发展担任应用程序开发人员的时期。

译自
What you learned
Key points
Challenge

你可能感兴趣的:(Hacking with iOS: SwiftUI Edition - 里程碑:项目 10 - 12)