譯自 Sending and receiving Codable data with URLSession and SwiftUI
更多內容,歡迎關注公眾號 「Swift花園」
喜歡文章?不如來個 ??➕三連?關注專欄,關注我 ???
用 URLSession 和 SwiftUI 傳送和接收 Codable 資料
iOS 為我們提供了用於從網路傳送和接收資料的內建工具,如果把它與Codable
支援結合使用,則可以將 Swift 物件轉換為 JSON 進行傳送,然後再接收回 JSON 並轉換回 Swift 物件。更好的是,當請求完成時,我們可以立即將資料賦給 SwiftUI 檢視中的屬性,從而觸發我們的使用者介面更新。
為了演示這一點,我們要從 Apple 的 iTunes API 中載入一些示例音樂的 JSON 資料,然後顯示在 SwiftUI 的List
中。Apple 的資料包含很多資訊,我們要削減成兩個型別:一個儲存曲目 ID ,名稱和所屬專輯的Result
,和一個儲存陣列結果的Response
。
從下面的程式碼開始:
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 給我們傳送一個泰勒·斯威夫特的所有歌曲的清單,然後用JSONDecoder
把結果轉換成一個Result
例項的陣列。
為了便於理解,讓我們分幾個階段進行程式設計。首先,是基本的方法 —— 把下面的存根新增到ContentView
結構體中:
func loadData() {
}複製程式碼
我們希望在顯示列表後立即執行它,因此,可以把下面這個 modifier 新增到List
:
.onAppear(perform: loadData)複製程式碼
在loadData()
內部,我們需要完成四個步驟:
- 建立我們要讀取的 URL。
- 用
URLRequest
包裝 URL,以便我們可以配置訪問 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
中。再次說明,URLRequest
是我們新增不同的自定義項以控制 URL 載入方式的地方,但是在這裡我們不需要任何內容,只有一行程式碼 —— 把它新增到loadData()
接下來的地方:
let request = URLRequest(url: url)複製程式碼
第三步是使用我們剛建立的URLRequest
建立並啟動網路任務。當你第一次看到它的時候,它看起來像是一個很奇怪的方法,而且它有一個特別常見的“陷阱” —— 你可能會在長達幾年的時間裡一次又一次地犯下錯誤。
我先向你展示程式碼,然後解釋它做了什麼 —— 把下面的程式碼新增到loadData()
:
URLSession.shared.dataTask(with: request) { data, response, error in
// 第四步
}.resume()複製程式碼
URLSession
是負責管理網路請求的 iOS 類。如果你願意,可以建立自己的會話,但普遍的做法是使用 iOS 建立給我們使用的shared
會話 —— 除非你需要某些特定的行為,否則使用這個共享會話就可以了。
然後,我們在這個共享會話上呼叫dataTask(with:)
,它從URLRequest
中建立一個網路任務,並在任務完成時執行一個閉包。在我們的程式碼中,這是用拖尾閉包語法提供的,你可以看到它接受三個引數:
data
是從請求返回的資料。response
是資料的描述,它可能包含資料的型別,資料量,是否有狀態碼,等等。error
是出現的錯誤。
狡猾之處在於其中的一些屬性是互斥的,我的意思是,如果發生error
,就不會設定data
,如果返回data
,則不會設定error
。之所以存在這種奇怪的狀態,是因為URLSession
API 是在 Swift 出現之前建立的,沒有更好的方法來表示這種二選一的狀態。
注意到我們立即在任務上呼叫resume()
的方式了嗎?這就是陷阱 —— 是你會一次又一次忘記的事情。沒有它,這個請求將什麼都不做,而你會盯著一個空白的螢幕。但是使用它,請求會立即開始,控制權移交到系統 —— 它會自動在後臺執行,即使我們的方法結束,也不會被破壞。
當請求完成,無論成功與否,第四步會開始生效 —— 這就是資料任務中的閉包,它負責對資料或錯誤進行處理。在我們的案例中,我們會檢查資料是否被設定,如果是則嘗試將其解碼為我們的Response
結構體的例項,因為這正是 iTunes API 傳送回來的資料。我們實際上並不想要整個響應,而只是想要其中的結果陣列,以便我們的List
將它們全部顯示出來。
不過,這裡還有另一個要點:URLSession
會在後臺自動執行,這意味著它的完成關閉閉包也會執行在後臺。所謂“後臺”,是指技術上稱為“後臺執行緒”的東西 —— 與我們程式的其餘部分同時執行的獨立程式碼段。這意味著,網路請求甚至可以執行幾秒鐘,而不會阻斷我們與 UI 的互動。
iOS 喜歡讓它的所有使用者介面工作都在“主執行緒”(程式啟動時的執行緒)上完成。這個機制會斷絕兩段程式碼同時操作使用者介面的可能性,因為如果所有與 UI 相關的工作都在主執行緒上進行,那麼衝突不可能發生。
我們希望將檢視的results
屬性修改為通過 iTunes API 下載的內容,然後反過來更新我們的使用者介面。在後臺執行緒上做這件事可能也工作地很好,因為SwiftUI 超級聰明。但老實說,我們不必冒險。在後臺獲取資料,在後臺解碼 JSON,然後在主執行緒實際更新屬性,是一個更好的主意。這樣可以避免任何潛在的問題。
iOS 提供了一種非常特殊的方式,用來將工作傳送到主執行緒:DispatchQueue.main.async()
。它接收一個執行工作的閉包,然後將其傳送到主執行緒以執行。從名稱中可以看出,實際上發生的事情是它被加入一個佇列 —— 一個等待執行的長隊伍。“async” 的部分是“非同步”的縮寫,表示我們自己的後臺工作不會等待閉包的執行;我們只是把它新增到佇列,然後繼續後臺的工作。
因此,用下面這段最後的程式碼替換// 第四步
的註釋:
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()
使用了可選鏈和空合運算子,如果存在錯誤則列印出錯誤,否則給出一個通用錯誤。
如果現在執行程式碼,你應該會在短暫的停頓後,看到 Taylor Swift 歌曲的列表 —— 考慮到最終結果的效果,我們的程式碼並不多。
在專案稍後的部分,我們將研究如何自定義URLRequest
,以便你可以傳送Codable
資料,但目前已經足夠了 —— 請將 ContentView.swift 還原回原始狀態,以便我們開始工作。
我的公眾號 這裡有Swift及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~