SwiftUI-网络请求URLSession

499 阅读6分钟

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()我们需要完成三个步骤:

  1. 创建我们想要读取的 URL。
  2. 获取该 URL 的数据。(睡眠可能发生)
  3. 将该数据的结果解码为Response结构体。

image.png

  • 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)