iOS 为我们提供了用于从互联网发送和接收数据的内置工具,如果我们将其与Codable支持结合起来,那么就可以将 Swift 对象转换为 JSON 进行发送,然后接收回 JSON 并将其转换回 Swift 对象。更好的是,当请求完成时,我们可以立即将其数据分配给 SwiftUI 视图中的属性,从而更新我们的用户界面。
关键字:async 和 await
iPhone 每秒都可以执行数十亿次操作 - 它的速度是如此之快,以至于在我们意识到它开始之前它就完成了大部分工作。另一方面,网络请求数据可能需要数百毫秒或更长时间,这对于当时用来执行数十亿其他事情的计算机来说非常慢。
- 异步函数
Swift 不会在网络请求发生时强制我们的整个进程停止,而是说“这项工作需要一些时间,所以请等待它完成,而应用程序的其余部分继续照常运行。” 此功能(在我们的主应用程序代码继续工作时,让某些代码保持运行的能力)称为异步函数。
异步函数是能够休眠一段时间的函数,以便它可以等待其他工作完成后再继续。
- 定义异步函数
func loadData() async {
// do something
}
注意async关键字——我们告诉 Swift 该函数可能需要进入睡眠状态以完成其工作 。
- 异步函数的调用时机
SwiftUI 为此类任务提供了不同的修饰符,并为其提供了一个特别容易记住的名称:task()。这可以调用可能会休眠一段时间的函数; Swift 要求我们做的就是用第二个关键字await 标记异步函数,明确承认可能会发生睡眠
List{
// ...
}
.task {
await loadData()
}
如果是按钮触发请求:
Button("下单"){
Task{
await loadData()
}
}
说“可能”是因为它可能不会——iOS 会对数据进行一些缓存,因此如果连续两次提取 URL,那么数据将立即发送回来,而不是触发睡眠。
加载网络数据
在里面 loadData()我们需要完成三个步骤:
- 创建我们想要读取的 URL。
- 获取该 URL 的数据。(睡眠可能发生)
- 将该数据的结果解码为Response结构体。
data(from:)方法
接受一个 URL 并返回该 URL 处的对象Data。返回值是一个元组,其中包含 URL 处的数据和一些描述请求如何进行的元数据。我们不使用元数据,但我们确实需要 URL 的数据,因此使用下划线 ,我们为数据创建一个新的本地常量,然后丢弃元数据。
此方法属于URLSession该类,如果需要,您可以手动创建和配置该类,但您也可以使用带有合理默认值的共享实例。
同时使用try和await时,我们必须写try await ,这是语法。
如果我们的下载成功,我们的data常量将被设置为从 URL 发回的任何数据,但如果由于任何原因失败,我们的代码将打印“无效数据”并且不执行任何其他操作。
上传
将创建一个URLRequest对象,然后将其配置为使用 HTTP POST 请求发送 JSON 数据。然后我们可以使用URLSession来上传我们的数据,并处理返回的任何内容。
func place() async {
// 入参
guard let encoded = try? JSONEncoder().encode(order) else { return
}
// 请求地址
let url = URL(string: "https://reqres.in/api/cupcakes")!
//
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
do {
// 发送 上传
let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
// 处理JSON,得到 数据实例
let decoded = try JSONDecoder().decode(Order.self, from: data)
} catch {
print("Checkout failed: \(error.localizedDescription)")
}
}
Result类型
Swift 提供了一种名为 Result 的特殊类型,它允许我们将成功的值或某种错误类型封装在单个数据中。
如果您查看 result 的类型,您会发现它是 一个 Result<String, Error> – 如果成功,它将包含一个字符串,但它也可能失败并包含错误。
struct ContentView: View {
@State private var output = ""
var body: some View {
Text(output)
.task {
await fetchReadings()
}
}
func fetchReadings() async {
do {
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
output = "Found \(readings.count) readings"
} catch {
print("Download error")
}
}
}
该代码工作得很好,但它并没有给我们很大的灵活性。
- 如果我们想将工作隐藏在某个地方,并在它运行时做其他事情怎么办?
- 如果我们想在将来的某个时刻读取它的结果,也许完全在其他地方处理任何错误怎么办?
- 或者如果我们只是因为不再需要而想取消该工作怎么办 ?
我们可以通过使用Result 来获得所有这些。
func fetchReadings() async {
let fetchTask = Task {
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
return "Found \(readings.count) readings"
}
}
我们之前用过Task 来启动工作片段,但在这里我们给了Task对象名称fetchTask——这给了我们额外的灵活性来传递它,或者在需要时取消它。请注意我们的Task闭包现在如何返回一个值?该值存储在我们的Task实例中,以便我们将来准备好时可以读取它。
更重要的是,如果网络获取失败,或者数据解码失败,则可能会引发错误,这就是Result出现的原因:我们Task任务的结果可能是一个字符串,表示“找到 10000 个读数”,但它也可能包含错误。找出答案的唯一方法是查看内部:
let result = await fetchTask.result
注意到我们没有习惯try读出输出吗?那是因为Result它保存在自身内部
如果需要,您可以直接从 读取成功值Result
do {
output = try result.get()
} catch {
output = "Error: \(error.localizedDescription)"
}
或者,您可以使用switch case 在Result, 上编写代码来检查成功和失败的情况。每一种情况都有其内部值(成功的字符串和失败的错误),因此 Swift 让我们使用特制的case匹配来读取这些值:
switch result {
case .success(let str):
output = str
case .failure(let error):
output = "Error: \(error.localizedDescription)"
}
加载网络图像
需要使用 SwiftUI的AsyncImage视图 。它使用图像 URL ,而不是简单的资源名称或 Xcode 生成的常量创建的,但 SwiftUI 会为我们处理剩下的所有事情 – 下载图像、缓存下载并自动显示它。
使用很简单
AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))
注意,在我们的代码运行并下载图像之前,SwiftUI 对图像一无所知,因此它无法提前适当调整图像的大小。
改进,调整图片和大小
AsyncImage(url: URL(string: "https://hws.dev/img/logo.png")) { image in
image
.resizable()
.scaledToFit()
} placeholder: {
ProgressView() // 自定义占位视图
}
.frame(width: 200, height: 200)
如果您想完全控制远程图像,可以使用第三种创建方法AsyncImage,它可以告诉我们图像是否已加载、遇到错误或尚未完成。当您想要在下载失败时显示专用视图时(如果 URL 不存在或用户离线等),这特别有用。
AsyncImage(url: URL(string: "https://hws.dev/img/bad.png")) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
} else if phase.error != nil {
Text("加载错误")
} else {
ProgressView()
}
}
.frame(width: 200, height: 200)