SwiftUI-特種鳥
-
在上一單元中,示範了三種鳥類的寫法,我們發現「陣列與 for迴圈」真是程式語言中的最佳拍檔,能夠聯手處理大量複雜資料,而且只要驗證過一兩筆,同樣的程式碼就可以處理無數筆同樣類型的資料,非常符合老子「道生一,一生二,二生三,三生萬物」的哲學。但是實際上隨著我們要處理的資料量愈來愈多,再依此種寫法繼續,程式碼就會變得很冗長,而且不容易維護。因此,本單元將介紹一種全新的寫法,稱為傑森(JSON)解碼器。
呃,問題就在資料如何源源不絕的進來?我們總不能將資料都寫在程式裡面吧!類似抖音、Instagram、Netflix以及很多一頁式網站,畫面格式是固定的,但內容可以不斷地更新,永遠都看不完,是怎麼做到的?
這就得靠「傑森」來幫忙。「傑森(JSON)」原來是 Javascript 程式語言用在動態網頁的一種資料傳輸格式,由網路後端伺服器將資料庫的結構化資料,轉換成 JSON格式,傳到使用者的瀏覽器上面,瀏覽器上面的Javascript程式再將JSON格式解構,依照欄位與設計架構呈現在畫面上,如下圖所示。利用這樣的方式,可以源源不絕地產生動態的畫面內容。
透過JSON檔案格式,我們可以將資料重複的部份列舉成類似字典的寫法,然後把資料存在程式碼外面,並且只要透過函式的呼叫,就可以取得外部預存的資料檔案。
實際上的JSON格式與程式碼非常接近,用程式來轉換非常方便,而且也很容易讓人閱讀或手動編輯,如下圖。因為簡單又方便,因此成為目前網路資料交換格式的業界標準,透過 JSON格式,程式就等於有一隻手延伸到雲端資料庫上面,資料自然可以源源不絕供應進來。
更棒的是,有越來越多的網站,開放其程式介面(Open API),也是使用JSON格式來交換資料,例如 Google map, Apple Music, Facebook 等等,國內的政府資料開放平台(Open Data)也部分提供JSON格式,因此,學會在程式中使用JSON格式,未來結合網路程式,就能延伸到各大網站,享受無窮盡的資料寶藏。
以台灣特有種鳥類為例,請下載以下這個JSON檔案,並將檔案名稱命名為「2-7 台灣特有種鳥類 - 2021.json」。
台灣特有種鳥類JSON檔下載網址: https://drive.google.com/file/d/13g2sCz-zXK4uCesWeY4pPflowb06qdus
2-7a 傑森解碼器
操作步驟如下:
- 選取畫面左方的「+」號新增電子書頁面。
- 將新增的電子書面頁命名為「(13)SwiftUI-7特種鳥」。
- 先點選畫面右方的「+」號新增檔案。
- 點選檔案
- 在「Main」模組中撰寫程式:
// 2-7a 傑森解碼器 // Created by Philip, Heman, Jean 2021/08/18 // Revised by Jean 2024/10/12 import Foundation struct 鳥類: Codable, Identifiable { var id: Int var 中文名: String var 別名: String var 科名: String var 英文名: String var 圖片檔名: String var 圖片來源: String var 攝影者: String } func 傑森解碼器(_ 檔名: String) -> [鳥類]? { if let 檔案 = Bundle.main.url(forResource: 檔名, withExtension: "json") { do { let 資料 = try Data(contentsOf: 檔案) let 結果 = try JSONDecoder().decode([鳥類].self, from: 資料) return 結果 } catch { print("error:\(error)") } } return nil } let 特有種清單 = 傑森解碼器("2-7 台灣特有種鳥類 - 2021") ?? [] for 鳥種 in 特有種清單 { print(鳥種.id, 鳥種.中文名, "\t", 鳥種.英文名, "\t", 鳥種.圖片來源) }
- 程式執行結果,如下圖。
這個範例程式不需要圖形介面,因此只要 import Foundation 即可,我們要從 Foundation 物件庫中取出的物件稱為 JSONDecoder (傑森解碼器),這個物件的用法如下:
let 結果 = try JSONDecoder().decode([鳥類].self, from: 資料)
JSONDecoder 必須先加空括號 () 產出一個實例,然後使用其解碼功能(方法) decode(),需要兩個參數:
1. 一個資料結構的型態,在此為[鳥類],即「鳥類」的陣列
2. 一個JSON資料格式的物件,在此為「資料」「資料」則是從我們匯入的檔案讀取出來,讀取的方法為:
let 檔案 = Bundle.main.url(forResource: 檔名, withExtension: "json") let 資料 = try Data(contentsOf: 檔案)
第一行的 Bundle 也是一個物件,是指跟程式一起包裹(bundle)的檔案目錄,我們所匯入的檔案,就在主要目錄(main)裡面,然後用 url() 方法來指定檔案位置,url 原本是用來定位網址,也可以用在檔案路徑,url() 需要兩個參數,分別為檔案名稱與副檔名,這裡要仔細填入(最好用複製貼上),不能有任何小錯。
第一行只是指定檔案位置,還未讀取,真正讀取檔案的是第二行,Data() 這個物件可以讀取檔案內容,產出一個 Data 物件實例,讓傑森解碼器轉換成我們需要的陣列。
函式傑森解碼器()其他的程式碼,包括 if, do-catch, try 等等,主要是為了處理讀取失敗的情況,暫時不必深入理解。
如果讀取並解碼成功,傑森解碼器()會傳回一個 [鳥類]? 的陣列,後面的問號 ? 在上一單元曾經使用過,這是個 Optional 資料類型,因為可能讀取失敗,會導致無法初始化(變數得不到值)。因此,在使用上,要多個注意:
let 特有種清單 = 傑森解碼器("2-7 台灣特有種鳥類 - 2021") ?? []
在後面我們增加一個 ?? [] ,如果傑森解碼器()傳回 nil 的話,就將「特有種清單」指定為空陣列 [],避免沒有初始化導致程式閃退。?? 和之前學過的 ? : 有點類似,都是方便在指定句中,根據條件選擇資料值的語法。
【註解】
1. JSON 全名是 "JavaScript Object Notation",意為 Javascript 物件表示法。內容是純文字組成,可以用文字編輯器手動編輯,或是用工具將Excel表格轉換為JSON檔案,但是千萬不要存成 Word 格式。
2. 傑森解碼 JSONDecoder().decode() 對於解碼的資料有一個要求,就是符合 Codable 規範,跟上一課 ForEach 要求 Identifiable 規範類似,Codable 規範要求資料結構必須由整數(Int)、實數(Float/Double)、字串(String)或其組成的結構(如日期Date)所組成,目前我們的「鳥類」結構是符合的,所以只須在宣告句加 Codable 關鍵字即可:struct 鳥類: Codable, Identifiable { var id: Int var 中文名: String var 別名: String var 科名: String var 英文名: String var 圖片檔名: String var 圖片來源: String var 攝影者: String }
3. 關於 struct 鳥類: Codable, Identifiable { } 的語法,在第1課曾經提過規範(protocol)是一個較大的類別,分類層級在 struct 定義的類型之上,因此,這個宣告句的意思是「鳥類」這個類型同時屬於 Codable 類別以及 Identifiable 類別之下。
2-7b 台灣特有種鳥類
有了傑森解碼器(JSON)之後,我們就不需要將資料寫入程式裡面,而是從外部檔案或網路資料庫讀取資料進來,程式裡面只要定義好 struct 資料結構並確認與 JSON 內容一致即可。
現在我們終於可以列出完整的30種台灣特有種鳥類。
- 在「Main」模組中撰寫程式:
// 2-7b 台灣特有種鳥類 // Created by Philip, Heman, Jean 2021/08/18 // Revised by Jean 2024/10/20 import PlaygroundSupport import SwiftUI struct 鳥類: Codable, Identifiable { var id: Int var 中文名: String var 別名: String var 科名: String var 英文名: String var 圖片檔名: String var 圖片來源: String var 攝影者: String } func 傑森解碼器(_ 檔名: String) -> [鳥類]? { if let 檔案 = Bundle.main.url(forResource: 檔名, withExtension: "json") { do { let 資料 = try Data(contentsOf: 檔案) let 結果 = try JSONDecoder().decode([鳥類].self, from: 資料) return 結果 } catch { print("error:\(error)") } } return nil } let 特有種清單 = 傑森解碼器("2-7 台灣特有種鳥類 - 2021") ?? [] for 鳥種 in 特有種清單 { print(鳥種.id, 鳥種.中文名, "\t", 鳥種.英文名, "\t", 鳥種.圖片來源) } struct 相框: View { var 檔名: String init(_ p: String) { 檔名 = p } var body: some View { if 檔名 == "" { Image(systemName: "camera.circle") .resizable() .scaledToFit() .foregroundColor(.red) .opacity(0.4) } else { Image(uiImage: UIImage(named: 檔名)!) .resizable() .scaledToFit() } } } struct 單項顯示: View { var 鳥: 鳥類 var body: some View { HStack { VStack { Text(鳥.中文名) .font(.title) .foregroundColor(.blue) ZStack(alignment: .bottomLeading) { 相框(鳥.圖片檔名) .frame(width: 120) Text("\(鳥.id)") .font(.title) .foregroundColor(.white) .shadow(color: .black, radius: 2, x: 0, y: 0) } } VStack(alignment: .leading) { Text("別名:" + 鳥.別名) Text("科名:" + 鳥.科名) Text("英文名稱:" + 鳥.英文名) Text("圖片來源:" + 鳥.圖片來源) Text("攝影者:" + 鳥.攝影者) } .font(.title2) .lineLimit(1) } .frame(height: 100) } } struct 台灣特有種鳥類: View { var body: some View { VStack { Rectangle() .frame(height: 75) .foregroundColor(.green) .overlay(Text("台灣特有種鳥類").font(.largeTitle)) ScrollView { ForEach(特有種清單) { 特有種 in 單項顯示(鳥: 特有種) .padding() } } } } } PlaygroundPage.current.setLiveView(台灣特有種鳥類())
與2-6b範例比較起來,我們再做最後的兩個修飾:
1. 「單項顯示」增加一個編號,字體用反白陰影,並以 ZStack 安排在圖片左下角位置。
ZStack(alignment: .bottomLeading) { 相框(鳥.圖片檔名) .frame(width: 120) Text("\(鳥.id)") .font(.title) .foregroundColor(.white) .shadow(color: .black, radius: 2, x: 0, y: 0)
2. 螢幕最上方增加「標題」方框。
Rectangle() .frame(height: 75) .foregroundColor(.green) .overlay(Text("台灣特有種鳥類").font(.largeTitle))
最後執行的結果如下圖,是不是與一開始的設計框架十分接近?