在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode 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
導覽頁:紐曼的技術筆記-索引