通过 Swift 的 async await 语法糖进行 URLSession

WWDC 21 过后,Swift 也推出了 async await 语法糖,并且麻烦的 URLSession.shared.dataTask 闭包 也拥有了新的语法糖版本

采用 async await 的 URLSession 能够有效的帮助我们避免在使用闭包时可能出现的线程问题

如果想了解完整的详情内容,可以查看 Apple Developer 的视频

这就是今天本文要提到的 URLSession.shared.data

URLSession.shared.data 支持两种参数,一种是 from 接受普通的 URL 类型并且快速进行 HTTP GET 请求

一种是 for 接受 URLRequest 类型请求,可以创建 URLRequest 类型的变量来对请求定义 HTTP 方法,Headers,Body 等等属性

如果你还没有了解 URLRequest,可以试试看上次的文章 在 Swift 5 和 SwiftUI 中通过 URLRequest 与 URLSession 请求 JSON 并反序列化 或者访问 Apple Developer Documentation

因为 URLSession.shared.data 的使用方式都一样,为了简单演示,在这里将会使用 URL 直接发送 GET 请求

我们可以在 Xcode 中建立一个 Swift Playground

image.png

一个美丽的空项目就这么被构建出来了

这篇文章采用一个 Mock API 作为演示,我们管它叫做 Movie Ticket,API 可以返回一些当日的电影票数据

它包括了

  • 电影名字
  • 电影类型
  • 电影票的价格
  • 电影时长 (分钟)

image.png

Swift 作为静态类型语言,我们需要先建立这个数据的结构体或者类

1
2
3
4
5
struct MovieTicket: Codable {
let movieTitle, movieGenre: String
let movieTicketPrice: Float
let movieDuration: Int
}

image.png

这将会是接下来进行 Decode 时候要用到的结构体,请注意,这个结构体必须要遵循 Decodable 协议,当然你也可以遵守 Codable 协议

接着,我们会建立一个 async function

1
2
3
func getTodaysMovie() async throws -> [MovieTicket] {

}

在使用全新的 URLSession.shared.data 的时候,错误将不会再放入以前的闭包中,而是使用 try 来获取错误,因此请在方法指纹中加入 throws

image.png

为了接下来可以抛出一些自定义的错误,我将创建一些我自己希望抛出的错误

1
2
3
4
enum MyError: Error {
case invlidUrl
case invlidServerResponse
}

当意外发生时,我可以 throw 出这些错误

image.png

就像这样,使用 throw 来代替 fatalError()

URLSession.shared.data(from: URL) 方法将会回传一个带有 dataresponse 的元组

因此通过如此描写来取回数据

1
let (data, response) = try await URLSession.shared.data(from: url)

如果不需要检查 response,可以使用 _ 来代替 response

在此,我们还是执行最佳做法,检查 response 中的 HTTP Status Code 来查看是否是正常的返回

1
2
3
4
5
6
guard
let response = response as? HTTPURLResponse,
response.statusCode == 200
else {
throw MyError.invlidServerResponse
}

我们需要先将 URLResponse 类型的 response 转换为 HTTPURLResponseresponse

并且检查 responsestatusCode 是否等于 200,不等于将会抛出自定的 MyError.invlidServerResponse 错误

检查完毕后,就可以将 data 通过 Swift 自带的 JSONDecoder 转换为可供我们访问的 MovieTicket 类型数据

1
2
3
let decoded = try JSONDecoder().decode([MovieTicket].self, from: data)

return decoded

因为服务器返回给我们的数据类型是多个 MovieTicket 的列表,所以使用中括号把 MovieTicket 包裹住,最后一步 return

好了,想看看服务器会返回什么数据吗

在这个方法外面的下面写点东西

1
2
3
4
5
6
7
Task {
let movieTickets = try await getTodaysMovie()

for movie in movieTickets {
print("Title: \(movie.movieTitle), Genres: \(movie.movieGenre)")
}
}

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Cocoa

struct MovieTicket: Codable {
let movieTitle, movieGenre: String
let movieTicketPrice: Float
let movieDuration: Int
}

enum MyError: Error {
case invlidUrl
case invlidServerResponse
}

func getTodaysMovie() async throws -> [MovieTicket] {
guard let url = URL(string: "https://my.api.mockaroo.com/movie_ticket.json?key=8795e870") else {
throw MyError.invlidUrl
}

let (data, response) = try await URLSession.shared.data(from: url)

guard
let response = response as? HTTPURLResponse,
response.statusCode == 200
else {
throw MyError.invlidServerResponse
}

let decoded = try JSONDecoder().decode([MovieTicket].self, from: data)

return decoded
}

Task {
let movieTickets = try await getTodaysMovie()

for movie in movieTickets {
print("Title: \(movie.movieTitle), Genres: \(movie.movieGenre)")
}
}