第71天 项目 14 第四部分
走到这一步花了不少时间,但今天你只需编写极少的代码就能实现一些应用功能——今天的任务应该相对轻松!
更具体地说,你将学习如何根据枚举值显示不同的UI界面,如何为从网络请求获取的数据添加自定义的Comparable协议遵循,以及更多实用技巧。这里包含一些重要的技术,它们能切实让我们的应用更上一层楼。
如果你觉得有些困难,不妨记住威廉·厄德利曾说过的一句话:“野心是通往成功的道路,坚持是抵达成功的交通工具。”你选择开始这门课程,本身就展现了野心;而如今你已经学到了第71天,显然也在坚持到底!
今天你需要学习两个主题,在学习过程中,你将进行网络请求、添加Comparable协议遵循等操作。
- 从维基百科下载数据
- 对维基百科的结果进行排序
从维基百科下载数据
作者:Paul Hudson 2024年1月7日
为了让整个应用更实用,我们要对EditView界面进行修改,使其能显示一些有趣的地点。毕竟,如果你的愿望清单中有“游览伦敦”这一项,你很可能希望得到一些附近值得一看的景点建议。这听起来似乎很难实现,但实际上我们可以通过GPS坐标查询维基百科,它会返回附近地点的列表。
维基百科的API会以特定格式返回JSON数据,因此我们需要做一些准备工作,定义能够存储这些数据的Codable结构体。数据结构如下:
- 主结果中,“query”键对应的值包含我们查询的结果。
- “query”内部有一个“pages”字典,键是页面ID,值是维基百科页面本身。
- 每个页面都包含大量信息,包括坐标、标题、术语等。
我们可以用三个相互关联的结构体来表示这种数据结构,因此创建一个名为Result.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中添加这个嵌套枚举:
enum LoadingState {
case loading, loaded, failed
}这三个case涵盖了表示网络请求所需的所有状态。
接下来,我们要给EditView添加两个属性:一个用于表示加载状态,另一个用于在数据获取完成后存储维基百科页面的数组。现在就添加这两个属性:
@State private var loadingState = LoadingState.loading
@State private var pages = [Page]()在处理网络请求本身之前,我们还有最后一件简单的事情要做:在Form中添加一个新的分区,以便在页面加载完成后显示这些页面,否则就显示状态文本视图。我们可以在Section中直接使用if/else if条件语句或switch语句,SwiftUI会自动处理。
因此,在现有分区下方添加这个分区:
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中添加这个方法:
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()修饰符:
.task {
await fetchNearbyPlaces()
}现在重新运行应用——你会发现,当你放下一个标记时,EditView界面会滑出,并显示附近所有的地点。很不错!
对维基百科的结果进行排序
作者:Paul Hudson 2024年1月7日
维基百科返回的结果顺序看似随机,但实际上是按照其内部的页面ID排序的。不过这种排序方式对我们没什么用,这就是为什么我们要用自定义的闭包来对结果进行排序。
很多时候,使用自定义排序函数正是我们所需要的,但更多情况下,数据会有一个自然的排序顺序——比如新闻按时间从新到旧排序,联系人按姓氏排序等。因此,我们不打算在sorted()方法中直接使用内联闭包,而是让Page结构体遵循Comparable协议。这其实很简单,因为我们已经写好了排序代码——只需把它移到Page结构体中即可。
首先,将Page结构体的定义修改为:
struct Page: Codable, Comparable {如果你还有印象,遵循Comparable协议只有一个要求:必须实现<函数,该函数接受两个与结构体类型相同的参数,如果第一个参数应该排在第二个参数之前,就返回true。在这个例子中,我们可以直接对title字符串进行比较,因此现在就在Page结构体中添加这个方法:
static func <(lhs: Page, rhs: Page) -> Bool {
lhs.title < rhs.title
}现在Swift知道如何对页面进行排序了,它会自动为页面数组提供一个无参数的sorted()方法。这意味着在fetchNearbyPlaces()方法中设置self.pages时,我们现在可以在末尾添加sorted(),如下所示:
pages = items.query.pages.values.sorted()在完成这个界面之前,我们需要把Text("页面描述内容")视图替换为真实的描述内容。维基百科的JSON数据中确实包含描述信息,但它藏得比较深:terms字典可能不存在;即使存在,也可能没有description键;就算有description键,其对应的值也可能是一个空数组,而非包含文本的数组。
我们不希望这些复杂情况影响SwiftUI代码,因此最好的做法是创建一个计算属性,该属性会在描述存在时返回描述,否则返回一个固定的字符串。在Page结构体中添加这个属性,完成结构体的定义:
var description: String {
terms?["description"]?.first ?? "无更多信息"
}完成这一步后,你就可以把Text("页面描述内容")替换为:
Text(page.description)到这里,EditView的开发就完成了——它允许我们编辑标注视图的两个属性,能从维基百科下载数据并排序,会根据网络请求的不同状态显示不同的UI界面,甚至还会仔细筛选维基百科的内容,以确定可显示的信息。