2024-06-14|閱讀時間 ‧ 約 31 分鐘

技術筆記-iOS實戰002-搞定 api 非同步呼叫,json 解碼,和一些檔案儲存的事

需求情境:

在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。

每次畫面一點的小修改,都會觸發 preview 重跑

每次畫面一點的小修改,都會觸發 preview 重跑


解決方案:

將後台傳回的資料以檔案形式暫存在本地端,每次 preview 都呼叫本地端檔案以節省時間和資源。再加上一個配套措施,就是由一個按鈕,觸發真正呼叫後台的程序,而呼叫後順便更新本地端檔案。以下開始實作。


實作步驟:

首先探討呼叫 api 的程序,檢討上一篇「iOS實戰001」所提的方法,有一個缺點,就是程序和 UI 元件混在一起,導致呼叫邏輯無法抽離到另一個檔案。因爲這是專案中經常會用到的通用程序,抽離才可以更好的共用,所以改用 async await 語法改寫,讓程序傳回通用型別 (Data) 的值:

func fetchApiDataAsync(urlString: String) async throws -> Data {
    guard let url = URL(string: urlString)
    else {
        throw MyError.error1
    }
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw MyError.error1
    }
    return data
}

這樣在任何一個畫面需要呼叫後台時,都可以在 onAppear() 事件中執行以下程序,取得資料後再執行與畫面相依的 decode 和 bind。以下 dashItems 就是宣告在畫面的狀態變數:

func getDashItems(urlString: String) {
    Task {
        do {
            let data = try await fetchApiDataAsync(urlString: urlString)
            let decoder = JSONDecoder()
            let dashItems = try decoder.decode([DashItem].self, from: data)                
            self.dashItems = dashItems // 此行把資料 bind 到畫面上
        } catch {
            print("Fail to fetch api data")
        }
    }
}


為了把回傳資料取出,用最原始的文字檔方式儲存在檔案,我們需要把 Data 的內容解開,轉換成為 String,我們把它包裝成另一個 function:

func fetchApiRawDataAsync(urlString: String) async throws -> String {
    guard let url = URL(string: urlString)
    else {
        throw MyError.error1
    }
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw MyError.error1
    }
    let rawString = String(data: data, encoding: .utf8)! // 關鍵是這一行
    return rawString
}


解開 String 之後,就可以存在檔案裡了,檔案儲存的位置有學問。依照 iOS 的運行規則,應用程式只可以把檔案存在一個特殊的資料夾稱為 DocumentDirectory,可用系統內建管理物件 FileManager 取得,我們把它包裝成另一個函數 getDocumentDirectory() ,供儲存檔案的函數呼叫:

func getDocumentDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
}

func saveDocumentFile(text: String) {
    let fileName = "cache.json"
    let filePath = getDocumentDirectory().appendingPathComponent(fileName)
    do {
        try text.write(to: filePath, atomically: true, encoding: String.Encoding.utf8)
    } catch {
        print("Error when writing file")
    }
}


寫入的檔案到底在哪兒?把路徑印出來長這樣:

file:///Users/newman/Library/Developer/Xcode/UserData/Previews/Simulator%20Devices/4BAD6318-2C87-4437-A00C-7EEFE7ED17F3/data/Containers/Data/Application/F4CC2965-6E39-400C-AA73-DD55FCB18E8C/Documents/cache.json

這雲深不知處的檔案,無法用 finder 查看內容是否正確,就作罷了,因為重點是若可以讀出來就達成目的了,因此先做一個讀出檔案的 function:

func loadDocumentFile(fileName: String) -> String {
    let data: Data
    let filePath = getDocumentDirectory().appendingPathComponent(fileName)
    do {
        data = try Data(contentsOf: filePath)
        return String(data: data, encoding: .utf8)!
    } catch {
        print("Couldn't load \(fileName) from document folder:\n\(error)")
        fatalError("Couldn't load \(fileName) from document folder:\n\(error)")
    }
}


經測試可正常顯示,所以以上會用到的功能都已經備齊了,可由畫面端程式自由組合顯示的邏輯。以下 getDashItemsFromFile() 供每次畫面顯示時呼叫,讀取本地端檔案可以非常快速。另外再加一個 refreshData() 由按鈕觸發,由使用者在必要時執行,呼叫後台並更新本地端檔案:

func getDashItemsFromFile() {
    Task {
        do {
            let rawString = loadDocumentFile(fileName: "cache.json")
            let decoder = JSONDecoder()
            let dashItems = try decoder.decode([DashItem].self, from: Data(rawString.utf8))
            self.dashItems = dashItems
        } catch {
            print("Fail to fetch api data")
        }
    }
}

func refreshData() {
Task {
        do {
            let rawString = try await fetchApiRawDataAsync(urlString: myApiUrl)
            saveDocumentFile(text: rawString)
            let decoder = JSONDecoder()
            let dashItems = try decoder.decode([DashItem].self, from: Data(rawString.utf8))
            self.dashItems = dashItems
        } catch {
            print("Fail to fetch api data")
        }
    }
}
  


Newman 2024/6/14

導覽頁:紐曼的技術筆記-索引


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.