昨天我們聊到第三方框架的導入,今天要回來看 Swift 本身的一個特性 —— async/await,並把它跟 網路請求 (Alamofire)、資料解析 (Codable) 串在一起,最後還會做一個小範例,把資料顯示在 SwiftUI 畫面上。這就是 App 串 API 的最小骨架。
在 iOS 開發裡,像是 API 請求、下載圖片、檔案存取,這些動作都需要時間,如果同步執行的話,App 就會「卡住」。所以我們必須用 非同步程式設計 來處理。
過去的 callback 方式很容易變成「callback hell」,讓程式不好維護,而 Swift 5.5 引入的 async/await 則讓我們可以用接近同步的語法,處理非同步邏輯。
URLSession
是 iOS 內建的網路工具,iOS 15 之後,官方已經支援 async/await
。
基本使用方式:
let (data, response) = try await URLSession.shared.data(from: url)
這樣就能直接拿到伺服器回傳的 data
。搭配 JSONDecoder
,就能轉成我們要的型別。
在 Swift 裡,Codable
是一個協定(protocol),其實是由 Encodable
和 Decodable
組合而成的:
也就是說,Codable
可以幫我們在「資料 ↔ Swift 物件」之間互轉。
假設 API 回傳的 JSON 如下:
{
"id": 1,
"title": "Hello Swift",
"body": "這是一篇測試文章"
}
我們只要定義一個 Codable
結構,就能輕鬆解析:
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
}
然後透過 JSONDecoder
:
let post = try JSONDecoder().decode(Post.self, from: data)
反過來,如果我們要把 Post
轉回 JSON:
let post = Post(id: 1, title: "Hello Swift", body: "這是一篇測試文章")
let jsonData = try JSONEncoder().encode(post)
我們使用 JSONPlaceholder 的測試 API:
https://jsonplaceholder.typicode.com/posts
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
}
這裡用 Alamofire 5.10.2,它已經支援 async/await
的 serializingDecodable
方法。
import Alamofire
class PostService {
func fetchPosts() async throws -> [Post] {
let url = "https://jsonplaceholder.typicode.com/posts"
return try await AF.request(url)
.serializingDecodable([Post].self)
.value
}
}
如果是URLSession的寫法
class PostService {
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
let posts = try JSONDecoder().decode([Post].self, from: data)
return posts
}
}
這樣的好處是:
data
,Alamofire 直接幫我們解碼成 [Post]
。(data, response)
。@MainActor
class PostViewModel: ObservableObject {
@Published var posts: [Post] = []
private let service = PostService()
func loadPosts() async {
do {
posts = try await service.fetchPosts()
} catch {
print("載入失敗:\(error)")
}
}
}
struct ContentView: View {
@StateObject private var viewModel = PostViewModel()
var body: some View {
NavigationView {
List(viewModel.posts) { post in
VStack(alignment: .leading, spacing: 8) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.vertical, 4)
}
.navigationTitle("文章列表")
.task {
await viewModel.loadPosts()
}
}
}
}
.task
是 SwiftUI 的一個修飾器,可以在畫面出現時執行非同步任務。今天我們學到:
serializingDecodable
就能拿到型別化的資料。URLSession
的細節,減少樣板程式。