網路基礎-生物篇
-
3-10a 台灣生物多樣性 Open Data (TBN)
本單元選用的內容是「政府開放資料平台(Open Data)」 -- 農委會特生中心(特有生物研究保育中心)的[「台灣生物多樣性網絡(TBN)」](https://www.tbn.org.tw/),這裏搜集了台灣2萬多種動植物的物種分類與1千多萬筆百年來的觀測紀錄,堪稱是台灣目前最完整的生態資料庫,最棒的是提供Open API開放給大眾使用。
根據[「台灣生物多樣性網絡」Open API說明文件](https://www.tbn.org.tw/data/api/openapi/v25),資料結構分為兩層,第一層有3個欄位:meta, links 與 data,第二層才是個別資料,我們需要的都在 data 裡面。對應的資料類型宣告如下:
struct 物種列表: Codable { var meta: 總數 var links: 頁次 var data: [物種資料] } struct 總數: Codable { var total: Int } struct 頁次: Codable { var next: String } struct 物種資料: Codable, Hashable { var taxonUUID: String //分類編號 var taxonName: String //分類名稱(中英文) var simplifiedScientificName: String //學名(拉丁文) var vernacularName: String //本地名稱(中文) var taxonRank: String //分類階層(中文) var family: String //科名(中英文) }
有了資料結構,就可以用「傑森解碼器」來取得資料,需要連結的網址格式範例為:
// 新版 v2.5 https://www.tbn.org.tw/api/v25/taxon?name=佛甲草&limit=30
* 基本上就是將「name=佛甲草」參數改為其他搜尋字串即可。
我們先仿照上一單元範例3-9a寫一個文字模式程式,來驗證資料結構與傑森解碼器是否正確,但改用「物件導向」的方法。
操作步驟如下:
選取畫面左方的「+」號新增電子書頁面。
將新增的電子書面頁命名為「(25)網路程式基礎-9生物篇」。
在「Main」模組中撰寫程式:
// 3-10a 台灣生物多樣性 Open Data (TBN) // Created by Philip, Heman, Jean 2022/01/31 // Revised by Jean 2025/01/20 // TBN Open Data:https://www.tbn.org.tw/data/api import Foundation struct 物種列表: Codable { var meta: 總數 var links: 頁次 var data: [物種資料] init() { meta = 總數(total: 0) links = 頁次(next: "") data = [] } mutating func 更新(_ 名稱: String) async { // Sample URL:"https://www.tbn.org.tw/api/v2/species?name=佛甲草&limit=30" // New:https://www.tbn.org.tw/api/v25/taxon?name=佛甲草&limit=30 if 名稱 == "" { return } var 合成網址 = URLComponents() 合成網址.scheme = "https" 合成網址.host = "www.tbn.org.tw" 合成網址.path = "/api/v25/taxon" 合成網址.query = "name=\(名稱)&limit=30" guard let myURL = 合成網址.url else { return } do { let (回傳資料, 回傳碼) = try await URLSession.shared.data(from: myURL) self = try JSONDecoder().decode(物種列表.self, from: 回傳資料) print(回傳碼) } catch { print("無法更新列表") } } } struct 總數: Codable { var total: Int } struct 頁次: Codable { var next: String } struct 物種資料: Codable, Hashable { var taxonUUID: String //分類編號 var taxonName: String //分類名稱(中英文) var scientificName: String //學名(拉丁文) var vernacularName: String //本地名稱(中文) var taxonRank: String //分類階層(中文) var family: String //科名(中英文) } Task { var 列表 = 物種列表() await 列表.更新("佛甲草") for 物種 in 列表.data { print(物種.vernacularName, 物種.scientificName) } } import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true
程式執行結果,如下圖。
NavigationView & extension
接下來仿照範例3-9c,製作一個NavigationStack 來顯示「台灣生物多樣性」的搜尋列表與詳細資料:
NavigationStack { if 列表.data == [] && name != "佛甲草" { Text("沒有資料🤷請重新搜尋") .font(.largeTitle) } else { List(列表.data, id: \.self) { 生物 in NavigationLink(destination: 生物.顯示詳細資料()) { Label(生物.vernacularName + 生物.simplifiedScientificName, systemImage: "leaf") .font(.title2) .lineLimit(1) } } .navigationTitle("台灣生物多樣性") } } .task { await 列表.更新(name) } .searchable(text: $name, placement: .navigationBarDrawer(displayMode: .always)) .onSubmit(of: .search) { Task { await 列表.更新(name) } }
在最後幾行,NavigationStack 用了3個修飾語,分別說明如下:
在這裡的搜尋,並不像上一課是搜尋列表本身的內容,而是用API連網搜尋新的列表,所以用 .onSubmit 而不是 .onChange,這樣的互動反應比較流暢。
.searchable 加了一個參數,placement: .navigationBarDrawer(displayMode: .always) 作用是將搜尋列固定顯示,而非預設的自動隱藏(列表向下滑才會出現)。
另外,這段程式碼還有一個不同之處,是本節的重點:
NavigationLink(destination: 生物.顯示詳細資料())
這裡的「顯示詳細資料()」同樣改用物件導向的方法來寫,將函式包在「物種資料」類型裡面,由物件實例「生物」來使用,不過比較特別的是,函式並不寫在「struct 物種資料」宣告裡面,而是用 "extension" 來「擴充物件」:
extension 物種資料 { func 顯示詳細資料() -> some View { VStack(alignment: .leading) { Image(systemName: "camera.viewfinder") .resizable() .scaledToFit() .opacity(0.1) .frame(width: 300) .padding() Text(self.vernacularName) Text(self.simplifiedScientificName) Text("資料來源:台灣生物多樣性網絡(TBN) https://www.tbn.org.tw/") Spacer() } } }
用extension跟直接寫在struct宣告裡面其實是一樣的作用,那麼又何必多此一舉呢?看似畫蛇添足,其實大有深意,用extension至少有兩個好處:
1. 當多人開發專案時,不用修改隊友寫的物件類型(程式可能在另一個檔案裡),就能加入自己想要的物件屬性或方法。
2. 更重要的是,還可以擴充系統原有的任何物件(總不可能拿到系統的原始碼吧)!也就是說,用extension可以擴充任何現存(已宣告過)的物件,例如讓 Text() 增加跑馬燈的動作,或是讓 Image() 具備AI辨識能力...等等,相對於函式化程式設計來說,物件的extension是全新的玩法,顛覆性的遊戲規則,而且簡單好用還異常強大,稱得上是物件導向的精髓。
在上面這段 extension 裡面,我們增加一個物件方法,會讀取物件屬性,用VStack排列出詳細資料的視圖作為回傳值,回傳值類型宣告為 some View,跟視圖主體(View body)用法相同。這裏用 self.vernacularName 來讀取屬性,self 指物件實例 -- 也就是NavigationLink裡面的「生物」。
這樣就完成了「台灣生物多樣性」程式,下一節再與「3-9c 台灣特有種鳥類」合併到一個App裡面。完整程式列表如下:
// 3-10b 台灣生物多樣性 NavigationView & extension // Last Revised Philip, Heman, Jean 2022/02/05 // Revised by Jean 2025/01/20 // TBN Open Data: https://www.tbn.org.tw/data/api import SwiftUI struct 物種列表: Codable { var meta: 總數 var links: 頁次 var data: [物種資料] init() { meta = 總數(total: 0) links = 頁次(next: "") data = [] } mutating func 更新(_ 名稱: String) async { // Sample URL: "https://www.tbn.org.tw/api/v2/species?name=佛甲草&limit=30" // New: https://www.tbn.org.tw/api/v25/taxon?name=佛甲草&limit=30 if 名稱 == "" { return } var 合成網址 = URLComponents() 合成網址.scheme = "https" 合成網址.host = "www.tbn.org.tw" 合成網址.path = "/api/v25/taxon" 合成網址.query = "name=\(名稱)&limit=30" guard let myURL = 合成網址.url else { return } do { let (回傳資料, 回傳碼) = try await URLSession.shared.data(from: myURL) self = try JSONDecoder().decode(物種列表.self, from: 回傳資料) print(回傳碼) } catch { print("無法更新列表") } } } struct 總數: Codable { var total: Int } struct 頁次: Codable { var next: String } struct 物種資料: Codable, Hashable { var taxonUUID: String //分類編號 var taxonName: String //分類名稱(中英文) var scientificName: String //學名(拉丁文) var vernacularName: String //本地名稱(中文) var taxonRank: String //分類階層(中文) var family: String //科名(中英文) } extension 物種資料 { func 顯示詳細資料() -> some View { VStack(alignment: .leading) { Image(systemName: "camera.viewfinder") .resizable() .scaledToFit() .opacity(0.1) .frame(width: 300) .padding() Text(self.vernacularName) Text(self.scientificName) Text("資料來源:台灣生物多樣性網絡(TBN) https://www.tbn.org.tw/") Spacer() } } } struct 台灣生物多樣性: View { @State var name = "佛甲草" @State var 列表 = 物種列表() var body: some View { NavigationView { if 列表.data == [] && name != "佛甲草" { Text("沒有資料🤷請重新搜尋") .font(.largeTitle) } else { List(列表.data, id: \.self) { 生物 in NavigationLink(destination: 生物.顯示詳細資料()) { Label(生物.vernacularName + 生物.scientificName, systemImage: "leaf") .font(.title2) .lineLimit(1) } } .navigationTitle("台灣生物多樣性") } } .task { await 列表.更新(name) } .navigationViewStyle(.stack) .searchable(text: $name, placement: .navigationBarDrawer(displayMode: .always)) .onSubmit(of: .search) { Task { await 列表.更新(name) } } } } import PlaygroundSupport PlaygroundPage.current.setLiveView(台灣生物多樣性())
程式執行結果,如下圖。