[SwiftUI 100 天] 登月计划 - part2 NavigationLink

561 阅读8分钟
译自 Pushing new views onto the stack using NavigationLink
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

用 NavigationLink 把新视图推到栈上

SwiftUI 的 NavigationView在我们的视图顶部显示一个导航栏,此外还做了一些其他的事情:它让我们可以把视图推进一个视图栈。实际上,这个机制是在 iOS 中导航用到的最基础的形式 —— 你可以在设置中看到:点击 Wi-Fi 或者通用,或者在消息应用里点击某人的名字。

这个视图栈系统与我们之前用过的 sheet 截然不同。是的,两者都可以显示新视图,但它们呈现的方式影响用户对于它们的感知。

那就开始编写代码吧,这样你可以亲眼证实。让我们用导航视图包装一个默认的文本视图,然后设置一个标题,得到这样的代码:

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World")
            }
            .navigationBarTitle("SwiftUI")
        }
    }
}

这个文本视图只是个静态文本:它不是可以附加某种动作的按钮,但我们会让用户点击 “Hello World” 时呈现一个新的视图,这要用到 NavigationLink:给它提供跳转终点和要点击的东西,剩下的事情它会解决。

我喜欢 SwiftUI 的诸多东西之一是我们可以把 NavigationLink应用于任意类型的终点视图。是的,我们可以设计一个自定义视图用作跳转,但也可以直接跳转到某个文本视图。

把视图代码改成这样,尝试一下:

NavigationView {
    VStack {
        NavigationLink(destination: Text("Detail View")) {
            Text("Hello World")
        }
    }
    .navigationBarTitle("SwiftUI")
}

运行代码,体验一下。你会发现 “Hello World” 现在看起来像一个按钮,点击它会从屏幕右侧滑入一个新的视图,呈现 “Detail View” 。更棒的是,你会发现标题 “SwiftUI” 以动画方式缩小成一个返回按钮,当你点击这个按钮,或者从屏幕左侧滑动,你可以回到之前的页面。

因此,虽然 sheet()NavigationLink 都能让我们从当前视图显示新视图,但它们实现这一点的方式不同,你需要谨慎选择:

  • NavigationLink 用于显示用户选择的细节,比如你正在深入某个主题。
  • sheet() 用于显示无关的内容,比如设置项或者某个组合窗口。

NavigationLink最常见于列表,在这里 SwiftUI 做了一些令人惊奇的事情:

尝试把我们的代码改成这样:

NavigationView {
    List(0..<100) { row in
        NavigationLink(destination: Text("Detail \(row)")) {
            Text("Row \(row)")
        }
    }
    .navigationBarTitle("SwiftUI")
}

运行 app ,你会看到 100 个列表行,都可以点击并显示细节视图,你还会在每项的右边看到一个灰色的箭头指示。这是一种 iOS 上的标准方式,箭头告诉用户,点击列表行,新的一屏会从右边滑入。SwiftUI 足够聪明,能自动添加这些指示。如果这些行不是可导航链接 —— 假如你注释掉 NavigationLink 行和对应的大括号 —— 你会发现箭头指示消失了。

译自 www.hackingwithswift.com/books/ios-s…

处理层级化的 Codable 数据

Codable 协议让解码扁平数据这件事变得不重要:如果你是在解码一个类型的单一实例,或者这种类型实例的数组或者字典,那么事情自然迎刃而解。不过,在这个项目中我们要解码稍微复杂一些的 JSON :有数组中的数组,并且使用了不同的数据类型。

如果你想解码这种层级化的数据,关键在于分离每一层的类型。只要数据匹配你要的层级, Codable 就能为我们解码所有东西,不用我们自己费工夫。

为了演示这一点,把下面的按钮放进你的视图:

Button("Decode JSON") {
    let input = """
    {
        "name": "Taylor Swift",
        "address": {
            "street": "555, Taylor Swift Avenue",
            "city": "Nashville"
        }
    }
    """

    // 更多代码
}

上面用代码创建了一条 JSON 字符串。以防你对 JSON 不是很熟悉,最好是看一下对应匹配 的 Swift 结构体:

struct User: Codable {
    var name: String
    var address: Address
}

struct Address: Codable {
    var street: String
    var city: String
}

希望你能看出 JSON 字符串包含了:一个用户,里面有一个名字字符串和一个地址,地址是街道字符串和城市字符串。

现在是最酷的部分:我们将把 JSON 字符串转换成 Data 类型(它是 Codable 可以处理的类型),然后再解码成一个 User 实例:

let data = Data(input.utf8)
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: data) {
    print(user.address.street)
}

运行程序,点击按钮,你应该会看到地址被打印出来 —— 谨防疑问,我要申明这不是她的实际地址!

对于 Codable 可以穿透的层级数量没有限制 —— 唯一重要的是你定义的结构体们和你的 JSON 字符串匹配。

这个项目的概览到此结束,接下来把 ContentView.swift 重置为原始状态。

译自 Loading a specific kind of Codable data

加载特定类型的 Codable 数据

在这个 app 中,我们要加载两种不同类型的 JSON 到 Swift 结构体:一个是给宇航员,另一个是给任务。用一种易于维护的方式完成这个目标,并且让代码不显得凌乱,需要一些思考,让我们一步一步实现。

首先,将两个 JSON 文件拖进项目。它们可以从本书的 Github 仓库里找到,在 “project8-files” 目录下,找到 astronauts.json 和 missions.json ,然后把它们拖进你的项目导航器, 当我们添加 assets 时,你还需要把所有的图像复制到你的 asset catalog —— 它们在 "Image" 子文件夹。这些宇航员和任务徽章的图片都是由 NASA 创建的,在 US Code 的 Title 17, Chapter 1, Section 105 ,这些图片可以用于公共领域。

先看一下 astronauts.json ,你会发现每名宇航员都是由三个字段定义:一个 ID(“grissom”,“white”,“chaffee”,等等),他们的名字(“Virgil I. "Gus" Grissom”, 等等),以及一个从维基百科复制的简短说明。如果你打算在你自己的项目中使用这些文本,你需要对这些词条和它们的作者给予肯定,并且明确这些东西是通过 CC-BY-SA 的授权方式使用,详见:creativecommons.org/licenses/by…

现在,让我们把宇航员数据转换成 Swift 结构体 —— 点击 Cmd+N 新建文件,选择 Swift 文件,然后起名为 Astronaut.swift,编写以下代码:

struct Astronaut: Codable, Identifiable {
    let id: String
    let name: String
    let description: String
}

如你所见,我让这个结构体遵循 Codable 协议,以便我们可以从 JSON 直接构建结构体实例,并且也遵循了Identifiable 协议,以便可以在 ForEach 使用宇航员数组 —— 只要有唯一的id字段就行。

接下来我们把 astronauts.json 转成一个 Astronaut 实例的数组,这意味着我们需要用Bundle来定位文件路径,加载为一个 Data实例,然后传给 JSONDecoder。之前我们把这些逻辑放在 ContentView里的一个方法里面,不过这里我要向你展示一种更好的方式:通过扩展 Bundle,把事情集中在一个地方完成。

再创建一个新的 Swift 文件,取名 Bundle-Decodable.swift ,几乎直接用了你之前见过的代码,不过有一点小小的区别:之前我们用 String(contentsOf:) 把文件内容载入一个字符串,但是因为 Codable 用的是 Data ,所以这里我们要换成 Data(contentsOf:)。它的工作方式跟 String(contentsOf:)一样:给它一个要加载的文件 URL ,它要么会返回文件内容,要么会抛出错误。

把下面的代码添加到 Bundle-Decodable.swift :

extension Bundle {
    func decode(_ file: String) -> [Astronaut] {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        guard let loaded = try? decoder.decode([Astronaut].self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}

代码中大量使用了 fatalError():如果文件找不到,无法加载或者无法解码,app 都会崩溃。不过,像之前一样,除非你犯错了,崩溃不会实际发生。举个例子,如果你忘了把 JSON 文件拷进项目。

你可能想知道为什么这里我们要用扩展而不是方法?理由是让加载 JSON 的逻辑更清晰。把下面这个属性添加 ContentView结构体:

let astronauts = Bundle.main.decode("astronauts.json")

就这一行代码。当然,我们所做的其实只是把代码从 ContentView 移到一个扩展中去,但这个动作不会有问题 —— 任何可以帮助我们控制视图的代码量和逻辑聚焦的动作都是好事情。

如果你想要确认 JSON 正确加载,可以把默认的文本视图修改成这样:

Text("\(astronauts.count)")

结果应该是显示 32 。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~