【譯】[SwiftUI 100 天] Cupcake Corner - part2

貓克杯發表於2020-04-07
譯自 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()內部,我們需要完成四個步驟:

  1. 建立我們要讀取的 URL。
  2. URLRequest 包裝 URL,以便我們可以配置訪問 URL 的方式。
  3. 基於這個 URL 請求建立並啟動網路任務。
  4. 處理網路任務的結果。

我們將從 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。之所以存在這種奇怪的狀態,是因為URLSessionAPI 是在 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及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~

【譯】[SwiftUI 100 天] Cupcake Corner - part2


相關文章