iOS为我们提供了从互联网发送和接收数据的内置工具,如果我们将其 Codable
持结合起来,那么就可以将 Swift 对象转换为 JSON 进行发送,然后将接收回来的 JSON 转换为 Swift 对象。更好的是,当请求完成时,我们可以立即将其数据分配给 SwiftUI 视图中的属性,从而导致用户界面更新。
为了演示这一点,我们可以从苹果的iTunes API中加载一些示例音乐JSON数据,并将其全部显示在一个SwiftUI List
中。苹果的数据包含很多信息,但我们将把它缩减为两种类型:一个Result
将存储一个曲目ID、名称和它所属的专辑,一个响应将存储一系列结果。
所以,从这段代码开始:
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var trackId: Int
var trackName: String
var collectionName: String
}
我们现在可以编写一个简单的ContentView
,它显示一个结果数组:
struct ContentView: View {
@State private var results = [Result]()
var body: some View {
List(results, id: \.trackId) { item in
VStack(alignment: .leading) {
Text(item.trackName)
.font(.headline)
Text(item.collectionName)
}
}
}
}
一开始不会显示任何内容,因为 results
数组是空的。这就是我们的网络调用的来源:我们将要求iTunes API向我们发送Taylor Swift的所有歌曲的列表,然后使用JSONDecoder
将这些结果转换为一组 Result
实例。
为了更容易理解,让我们分几个阶段来写。首先,这里是基本方法——请将其添加到ContentView
结构中:
func loadData() {
}
我们希望在显示列表时立即运行该操作,因此您应该将此修饰符添加到List
中:
.onAppear(perform: loadData)
在loadData()
中,我们需要完成四个步骤:
- 创建我们要读取的URL。
- 将其包装在
URLRequest
中,这允许我们配置如何访问URL。 - 从该URL请求创建并启动网络任务。
- 处理网络任务的结果。
我们将从URL开始逐步添加这些内容。这需要有一个精确的格式:“itunes.apple.com”和一系列的参数——如果你在网上搜索“iTunes Search API”,你可以找到完整的参数集。在我们的例子中,我们将使用搜索词“Taylor Swift”和实体“song”,因此现在将其添加到loadData()
中:
guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
print("Invalid URL")
return
}
接下来,我们需要将该URL包装成一个URLRequest
。同样,在这里,我们将添加不同的自定义项来控制加载URL的方式,但在这里我们不需要任何东西,因此这只是一行代码——接下来将其添加到loadData()
中:
let request = URLRequest(url: url)
第3步是使用我们刚才发出的URLRequest
创建并启动一个网络任务。当你第一次看到它的时候,你会觉得这是一个相当奇怪的方法,它有一个特别常见的“陷阱”——一个你会犯一次又一次的错误,而且可能几年后还会犯。
我将首先向您展示代码,然后解释它的作用——将下方代码添加到loadData()
:
URLSession.shared.dataTask(with: request) { data, response, error in
// step 4
}.resume()
URLSession
是负责管理网络请求的iOS类。如果愿意,您可以创建自己的会话,但通常使用iOS创建的共享会话供我们使用,除非您需要某些特定行为,否则使用共享会话是可以的。
然后,我们的代码在该共享会话上调用dataTask(with:)
,这将从URLRequest
创建一个网络任务,并在任务完成时运行一个闭包。在我们提供的代码中使用尾随闭包语法,可以看到它接受三个参数:
-
data
是从请求返回的任何数据。 -
response
是对数据的描述,其中可能包括数据的类型、发送了多少数据、是否有状态代码等等。 -
error
是发生的错误。
现在,巧妙地说,其中一些属性是互斥的,我的意思是,如果发生错误,那么就不会设置data
,如果data
被发送回去,那么就不会设置error
。这种奇怪的状态之所以存在,是因为URLSession
API是在Swift出现之前(OC时代)生成的,所以没有更好的方式来表示这种状态。
注意到我们直接对任务调用resume()
的方式了吗?这就是问题所在——这是你会一次又一次忘记的事情。如果没有它,请求什么也做不了,你会盯着一个空白的屏幕。但有了它,请求立即开始,控制权移交给系统——它将自动在后台运行,即使在我们的方法结束后也不会被破坏。
当请求完成时,无论成功与否,都会进入第4步——这是数据任务中的闭包,负责处理数据或错误。在我们的例子中,我们将检查数据是否已设置,以及是否尝试将其解码为响应结构的实例,因为这是iTunes API返回的。实际上,我们并不需要整个Response
,只需要其中的结果数组,这样我们的列表将显示它们。
不过,这里还有一个陷阱:URLSession
自动在后台运行,这意味着它的完成闭包也将在后台运行。我所说的“后台”是指在技术上称为后台线程的代码,它是一段独立的代码,与程序的其他部分同时运行。这意味着网络请求可以运行,甚至需要几秒钟,而不会阻止我们的UI进行交互。
iOS需要在主线程上完成所有的用户界面相关工作,主线程是程序启动的地方。因为如果所有与UI相关的工作都发生在主线程上,这将停止两段试图同时操作用户界面的代码,那么它就不会发生冲突。
我们希望将视图的results
属性更改为通过iTunes API下载的内容,然后更新我们的用户界面。这在后台线程上可能很有用,因为SwiftUI是超级智能的,但老实说,这不值得冒险——最好是在后台获取数据,在后台从JSON解码,然后在主线程上更新相关属性,以避免任何潜在的问题。
iOS为我们提供了一种向主线程发送工作的非常特殊的方式:DispatchQueue.main.async()
。这需要一个要执行的工作闭包,并将其发送到主线程执行。从它的名字可以看出,实际发生的是它被添加到一个队列中——一个等待执行的大工作队列。“async”部分是“asynchronous”的缩写,这意味着我们自己的后台工作不会等待闭包运行;我们只是将它添加到队列中,然后在后台继续工作。
所以,将最后一个代码放在 //step 4
注释的位置:
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// 我们得到了有用的数据 - 返回到主线程
DispatchQueue.main.async {
// 更新我们的UI
self.results = decodedResponse.results
}
// 所有工作都完成了,所以可以退出了
return
}
}
// 如果代码跑到这里了,说明发生了某些错误
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
最后一行print()
使用可选的链接和空合运算符,以确保在存在错误时打印错误,否则给出一个一般错误。
如果你现在运行代码,你应该会看到一个泰勒·斯威夫特歌曲的列表在短暂的停顿之后出现——考虑到最终结果的效果,这真的不是很多代码。
稍后在这个项目中,我们将研究如何自定义URLRequest
,以便您可以发送可编码的数据,但现在这已经足够了——请将ContentView.swift
重置为其原始状态,以便我们可以开始工作。
译自 Sending and receiving Codable data with URLSession and SwiftUI
SwiftUI:为 @Published 属性添加 Codable 支持 | Hacking with iOS: SwiftUI Edition | SwiftUI:验证和禁用表单 |
---|
赏我一个赞吧~~~