Skip to content

第71天 项目 14 第四部分

走到这一步花了不少时间,但今天你只需编写极少的代码就能实现一些应用功能——今天的任务应该相对轻松!

更具体地说,你将学习如何根据枚举值显示不同的UI界面,如何为从网络请求获取的数据添加自定义的Comparable协议遵循,以及更多实用技巧。这里包含一些重要的技术,它们能切实让我们的应用更上一层楼。

如果你觉得有些困难,不妨记住威廉·厄德利曾说过的一句话:“野心是通往成功的道路,坚持是抵达成功的交通工具。”你选择开始这门课程,本身就展现了野心;而如今你已经学到了第71天,显然也在坚持到底!

今天你需要学习两个主题,在学习过程中,你将进行网络请求、添加Comparable协议遵循等操作。

  • 从维基百科下载数据
  • 对维基百科的结果进行排序

从维基百科下载数据

作者:Paul Hudson 2024年1月7日

为了让整个应用更实用,我们要对EditView界面进行修改,使其能显示一些有趣的地点。毕竟,如果你的愿望清单中有“游览伦敦”这一项,你很可能希望得到一些附近值得一看的景点建议。这听起来似乎很难实现,但实际上我们可以通过GPS坐标查询维基百科,它会返回附近地点的列表。

维基百科的API会以特定格式返回JSON数据,因此我们需要做一些准备工作,定义能够存储这些数据的Codable结构体。数据结构如下:

  • 主结果中,“query”键对应的值包含我们查询的结果。
  • “query”内部有一个“pages”字典,键是页面ID,值是维基百科页面本身。
  • 每个页面都包含大量信息,包括坐标、标题、术语等。

我们可以用三个相互关联的结构体来表示这种数据结构,因此创建一个名为Result.swift的新Swift文件,并添加以下内容:

swift
struct Result: Codable {
    let query: Query
}

struct Query: Codable {
    let pages: [Int: Page]
}

struct Page: Codable {
    let pageid: Int
    let title: String
    let terms: [String: [String]]?
}

我们将使用这些结构体存储从维基百科获取的数据,然后立即在UI界面中显示。不过,在数据获取过程中,我们需要显示一些内容——比如一个显示“Loading”(加载中)的文本视图就可以。

这意味着要根据当前的加载状态有条件地显示不同的UI,这就需要定义一个枚举来“存储”当前的加载状态,否则我们无法确定该显示什么内容。

首先,在EditView中添加这个嵌套枚举:

swift
enum LoadingState {
    case loading, loaded, failed
}

这三个case涵盖了表示网络请求所需的所有状态。

接下来,我们要给EditView添加两个属性:一个用于表示加载状态,另一个用于在数据获取完成后存储维基百科页面的数组。现在就添加这两个属性:

swift
@State private var loadingState = LoadingState.loading
@State private var pages = [Page]()

在处理网络请求本身之前,我们还有最后一件简单的事情要做:在Form中添加一个新的分区,以便在页面加载完成后显示这些页面,否则就显示状态文本视图。我们可以在Section中直接使用if/else if条件语句或switch语句,SwiftUI会自动处理。

因此,在现有分区下方添加这个分区:

swift
Section("附近的地点…") {
    switch loadingState {
    case .loaded:
        ForEach(pages, id: \.pageid) { page in
            Text(page.title)
                .font(.headline)
            + Text(": ") +
            Text("页面描述内容")
                .italic()
        }
    case .loading:
        Text("加载中…")
    case .failed:
        Text("请稍后再试。")
    }
}

提示: 注意到我们可以用+将文本视图拼接起来了吗?这样我们就能创建格式多样的复合文本视图。这里的“页面描述内容”只是临时占位文本——我们很快就会替换它。

现在,关键的一步来了:我们需要从维基百科获取数据,将其解码为Result类型,把其中的页面数据赋值给pages属性,然后将loadingState设置为.loaded。如果获取失败,就将loadingState设置为.failed,SwiftUI会相应地加载对应的UI界面。

警告: 我们需要加载的维基百科URL非常长,与其手动输入,不如从本文或我的GitHub gist(链接:http://bit.ly/swiftwiki)中复制粘贴。

EditView中添加这个方法:

swift
func fetchNearbyPlaces() async {
    let urlString = "https://en.wikipedia.org/w/api.php?ggscoord=\(location.latitude)%7C\(location.longitude)&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=json"

    guard let url = URL(string: urlString) else {
        print("无效的URL:\(urlString)")
        return
    }

    do {
        let (data, _) = try await URLSession.shared.data(from: url)

        // 成功获取到数据!
        let items = try JSONDecoder().decode(Result.self, from: data)

        // 成功——将字典的值转换为页面数组
        pages = items.query.pages.values.sorted { $0.title < $1.title }
        loadingState = .loaded
    } catch {
        // 如果执行到这里,说明请求在某种程度上失败了
        loadingState = .failed
    }
}

这个请求需要在视图出现时就开始执行,因此在现有的toolbar()修饰符之后添加这个task()修饰符:

swift
.task {
    await fetchNearbyPlaces()
}

现在重新运行应用——你会发现,当你放下一个标记时,EditView界面会滑出,并显示附近所有的地点。很不错!

对维基百科的结果进行排序

作者:Paul Hudson 2024年1月7日

维基百科返回的结果顺序看似随机,但实际上是按照其内部的页面ID排序的。不过这种排序方式对我们没什么用,这就是为什么我们要用自定义的闭包来对结果进行排序。

很多时候,使用自定义排序函数正是我们所需要的,但更多情况下,数据会有一个自然的排序顺序——比如新闻按时间从新到旧排序,联系人按姓氏排序等。因此,我们不打算在sorted()方法中直接使用内联闭包,而是让Page结构体遵循Comparable协议。这其实很简单,因为我们已经写好了排序代码——只需把它移到Page结构体中即可。

首先,将Page结构体的定义修改为:

swift
struct Page: Codable, Comparable {

如果你还有印象,遵循Comparable协议只有一个要求:必须实现<函数,该函数接受两个与结构体类型相同的参数,如果第一个参数应该排在第二个参数之前,就返回true。在这个例子中,我们可以直接对title字符串进行比较,因此现在就在Page结构体中添加这个方法:

swift
static func <(lhs: Page, rhs: Page) -> Bool {
    lhs.title < rhs.title
}

现在Swift知道如何对页面进行排序了,它会自动为页面数组提供一个无参数的sorted()方法。这意味着在fetchNearbyPlaces()方法中设置self.pages时,我们现在可以在末尾添加sorted(),如下所示:

swift
pages = items.query.pages.values.sorted()

在完成这个界面之前,我们需要把Text("页面描述内容")视图替换为真实的描述内容。维基百科的JSON数据中确实包含描述信息,但它藏得比较深:terms字典可能不存在;即使存在,也可能没有description键;就算有description键,其对应的值也可能是一个空数组,而非包含文本的数组。

我们不希望这些复杂情况影响SwiftUI代码,因此最好的做法是创建一个计算属性,该属性会在描述存在时返回描述,否则返回一个固定的字符串。在Page结构体中添加这个属性,完成结构体的定义:

swift
var description: String {
    terms?["description"]?.first ?? "无更多信息"
}

完成这一步后,你就可以把Text("页面描述内容")替换为:

swift
Text(page.description)

到这里,EditView的开发就完成了——它允许我们编辑标注视图的两个属性,能从维基百科下载数据并排序,会根据网络请求的不同状态显示不同的UI界面,甚至还会仔细筛选维基百科的内容,以确定可显示的信息。

本站使用 VitePress 制作