筆記:Develop in Swift Data Collections_Swift Generics

結合前面學的Protocol然後搭配泛型概念的運用,確實讓程式碼的整潔性更好惹!

· 學習目標
· 基礎概念
· 1.Generic Types
· 2.Generic Functions
2–1.Naming Type Parameters
· 3.Protocols and Associated Types
3–1.設計protocol
3–2.方法的實現
3–3.為 PhotoInfo 結構實現符合 APIRequest 的型別
3–4.流程回顧
· 4.獲取和查看實際圖片
· Lab-Life-Form Search
1.User Interface Design
2.Search API
2–1.解析 Search API 回傳的JSON結構建立
2–2.建立用於處理對「EOL Search API」的請求
3.Pages API
3–1.解析 Pages API 回傳的結構建立
3–2.建立用於處理對「EOL Pages API」的請求
· 4.Hierarchy API
4–1.解析 Hierarchy API 回傳的結構建立
4–2.建立用於處理對「EOL Hierarchy API」的請求
· 5.Image Request
· 6.Issuing the API Requests
GitHub

學習目標

  • 參考
  • 泛型可以創建靈活且可重複使用的函式和型別,它們可以適用於任何型別,讓相同的函式或型別可以適用於不同的數據型別,這樣可以減少重複的程式碼,並讓程式碼更加清晰易懂。
  • 在Swift裡頭,像是ArrayDictionary這類基本型別背後,都是靠著泛型來提供統一的API和功能,同時又不會讓程式碼重複。
1. 理解透過泛型讓程式碼可以重複使用。
2. 將泛型和協定結合應用。
3. 在協定中使用相關型別。

基礎概念

  • Associated Types(相關型別)
1. 協定(Protocols)可以使用相關型別(Associated Types)來提供泛型功能。

2. Associated Types 讓 Protocols 不用指定具體的型別,而是提供一個將來會用到的型別的占位符。

EX: Sequence協定 裡的 Element 讓我們知道序列能包含哪種型別的元素。
當我們建立一個[Int]陣列的時候,實際上確定了Element為Int型別。
  • Typealias
1. typealias 可以為現有的型別取一個新名字,增加程式碼的可讀性。

2. 在處理複雜的「泛型」或「閉包」時,typealias能讓程式碼更易讀。

3. 實作有 Associated Types 的 Protocols 時,使用 typealias 來定義這些型別是很常見的做法。
  • Type Parameter(型別參數)
Type Parameter 是個臨時的代替符號,在泛型宣告中用來代表將來會指定的具體型別。

EX: 一個泛型函式func abs<T>(_ x: T) -> T用來計算一個數字的絕對值,這裡的T就是型別參數,
它在使用這個函式時會被實際呼叫的型別所取代,比如在abs(-42)這個呼叫中,T就對應到Int型別。


1. 型別參數是泛型宣告時的佔位符號。

2. 它代表實際的型別,會在「泛型函式」或「型別被使用」時才確定。

3. 例如在泛型函式abs中,T就是等待被實際型別(如Int)替換的型別參數。

4. 型別參數讓「泛型函式」或「型別」能夠適用於「多種型別」。

1.Generic Types

1. 宣告陣列時,要麼提供具體的值,要麼指定它將包含哪種型別的數據。

2. 編譯器能夠根據提供的值來推斷陣列將包含的數據型別。
編譯器就知道這是Int型別的陣列。
寫法上的簡化,比如用[Int]()代替Array<Int>()
  • 泛型型別使用尖括號<>來宣告,括號內放上型別名稱。
這裡的Element不是指特定的型別。它是一種型別參數,當宣告一個陣列時,
實際上就是用一個具體的型別來取代Element。
  • 另一種泛型型別是Dictionary
一個 Dictionary 有與 value 相關聯的 key,並且在初始化字典時定義了兩者的類型。
如果要創建一個空的字典並隨著時間添加值,需要明確定義型別
  • Dictionary型別的宣告類似
用Key和Value兩個型別參數來定義,並且用where來限定鍵必須是可以 Hashable 的

2.Generic Functions

  • 泛型函數可以跟不同的型別合作,像是 max(_:_:) 函數,就可以比較兩個相同型別值的大小,回傳較大的那個值。
1. 泛型函數讓我們可以用「同一個函數」處理不同的型別。
EX: max(1, 10) 會回傳 10,因為它比較了兩個 Int 型別的值。

2. 泛型函數的定義中會用 <T> 這樣的語法,T 是一個「型別參數」,代表呼叫時會用到的實際型別。

3. 在 max 函數中,所有的「參數」和「回傳值」都要是「同一型別 T」,
而且這個 T 需要是可以比較大小的型別,這是透過 where T : Comparable 條件來限定的。

2–1.Naming Type Parameters

1. 型別參數 T 只是「Type」的縮寫,沒有具體含義。

2. 盡可能使用有意義的名字命名型別參數,並遵循「大駝峰式」的命名規則。
- EX: Array<Element> 和 Dictionary<Key, Value> 是命名型別參數的好例子。

3. 如果找不到合適的名字,使用 T 或字母順序的其他字母作為型別參數是可以的。

3.Protocols and Associated Types

  • 我們也可以創建泛型協定。類似於泛型類型或泛型函數,泛型協定使用一個需要被具體型別填充的相關型別(Associated Type)。然後可以在「協定」定義的屬性或方法中使用這個「相關型別」。
假設想創建一個協定,用來定義發送API請求和解碼回應的角色。
- 我們協定需要有一個變數來持有請求,以及一個處理回應的方法。
- 這個方法會接收數據,但它應該回傳什麼?這時候就可以使用相關型別。

1. associatedtype 是用來在協定中定義一個將要替代的型別。

2. 任何採用了泛型協定的型別都需要為這個佔位符指定一個具體型別。
EX: 當實作一個處理API回應的協定時,會用實際的「數據型別」來取代 「Response」 這個相關型別。
  • 以先前的練習為例子:
兩個方法中的類似過程:
- 創建一個 URLComponents 的實例。
- 使用 URLComponents 構建的URL發起數據請求。
- 如果請求成功,則將回應解碼為預期型別並回傳。如果數據請求或解碼失敗,則拋出錯誤。

可以寫一個「泛型」來覆蓋多種情況。
- 好處是可以減少程式碼——從而減少潛在的錯誤。
- 如果一段邏輯包含一個錯誤,那麼該邏輯的任何副本都會包含相同的錯誤。
之前從NASA的天文圖片API中獲取了資訊和圖片

3–1.設計protocol

  • 當在思考要寫一個Protocol時,會有一些不確定的因素:
1. URL和任何特定於請求的資訊。
2. 在完成處理程序(Completion Handler)中期望的結果型別。

雖然請求的內容會有所不同,但 URLRequest 本身是一個固定的型別。
然而,還需要知道將被回傳的型別是什麼,這就是相關型別(Associated Types)發揮作用的地方。
定義一個包含 Associated Types 的協定APIRequest

3–2.方法的實現

  • 。在創建一個符合協定的型別之前,最好先寫出一個使用這個協定的方法,這樣可以確保我們已經準備好了所有需要的部件。
兩種寫法中,Request 是一個泛型型別參數,它必須遵循 APIRequest 協定。
- 函式接受任何遵循 APIRequest 協定的實例作為參數,使用該實例的 urlRequest 來發送HTTP請求。
- 檢查HTTP響應的狀態碼,確保請求成功。
- 然後使用該實例的 decodeResponse 方法來解碼數據,最終回傳解碼後的響應。
  • 這個方法的結構模仿了之前的fetchPhotoInfo(),但這裡並沒有具體提到PhotoInfo。傳入的請求參數通過APIRequest協定提供了必要的資訊。這段代碼的重用性比fetchPhotoInfo()來得更高,因為它不依賴於任何特定的數據結構。

3–3.為 PhotoInfo 結構實現符合 APIRequest 的型別

  • 先定義PhotoInfo
  • PhotoInfoAPIRequest 實現 APIRequest 協定,定義了如何從 NASA API 獲取資料解碼回應
在 PhotoInfoAPIRequest 的實作中,會用 urlRequest 計算屬性來建立一個請求。
decodeResponse(data:) 將數據解碼為 PhotoInfo。

當指定 decodeResponse(data:) 的回傳型別為 PhotoInfo 時,Swift 就會自動推斷出
Response 的具體型別為 PhotoInfo。
  • 發送請求
1. 只要 APIRequest 和相關的 PhotoInfoAPIRequest 正確實現,
sendRequest 函式將能夠處理任何符合 APIRequest 的請求。

2. 在 Task 中,如果 sendRequest 函式因為一個非200的HTTP響應而拋出錯誤,
將會被捕獲並打印為 APIRequestError.itemNotFound。

3–4.流程回顧

  • 建立一個帶有「相關型別(associatedtype)的協定,一個利用該協定的「泛型方法」,以及一個實作該協定的「具體型別」,並使用它們從網路服務獲取數據。

4.獲取和查看實際圖片

  • 發送請求並獲取圖像
符合 APIRequest 協定。它定義了必要的 urlRequest 屬性來創建一個網絡請求。
以及 decodeResponse(data:) 方法來處理從該請求返回的數據。
如果從返回的數據中無法創建一個 UIImage,則 decodeResponse(data:) 方法會拋出錯誤。
  • 接收照片資訊
首先使用 PhotoInfoAPIRequest 來發送一個獲取NASA每日一圖的請求。
一旦獲得照片的資訊,它接著使用這些資訊中的URL來創建一個 ImageAPIRequest。
然後發送第二個請求以獲得實際的圖片。
如果在請求過程中發生錯誤,如API回傳非200狀態碼或數據無法正確解碼為圖片,則會捕獲錯誤。
  • 整理

Lab-Life-Form Search

  • 建立一款App能讓使用者搜尋各種生物並展示它們分類訊息和圖片。
實作Codable協定來簡化和轉換API返回的複雜數據。
運用 EOL API提供的不同端點,運用新學到的「泛型」來優化程式碼的絕佳時機。
  • 建立相關型別(associatedtype)的協定
通過「泛型」和「協定」,可以輕鬆適應不同的API請求和數據格式,同時保持一致的結構和處理方式。
  • 利用該協定的「泛型方法
建立一個可重用的API請求處理器,它可以處理多種不同的API請求,而不僅僅局限於一種特定的請求或數據結構。通過定義一個泛型方法sendRequest,您可以使用相同的邏輯來處理符合APIRequest協定的任何請求,從而提高了代碼的重用性和可維護性。使用單例模式確保了整個應用中只有一個EOLController實例,這有助於管理和維護與EOL API的通訊。


1. 共享訪問:
- App 的任何部分都可以訪問這個唯一的實例,這樣可以確保所有對API的請求和回應都是協調一致的。

2. 重用泛型方法:
- sendRequest 泛型方法,可以處理符合 APIRequest 協定的任何類型的請求。
- 不同的API請求可以使用相同的方法來處理,而不需要為每種不同的請求撰寫重複的代碼。

1.User Interface Design

  • 一個是來自EOL資料庫搜尋的結果列表。
  • 一個是當使用者選擇某個搜尋結果時相關聯的詳細資訊。
  • API 來源
1. pages API提供基本資訊以及對一個或多個圖片和分類選項的引用清單。
2. hierarchy_entries API提供特定分類方案的細節。
3. pages API的結果包含了獲取項目圖片的URL。
  • 設置Data擴展,改善回傳資訊的展示
視覺化每個API請求返回的結果。

2.Search API

2–1.解析 Search API 回傳的JSON結構建立

根據回傳的Json格式。
主要關注的是results陣列中的資料,以及其中每一個物件的id、title和content。

2–2.建立用於處理對「EOL Search API」的請求

透過實現 APIRequest 協定,EOLSearchAPIRequest 能夠封裝所有與發送請求、
接收和解碼回應相關的邏輯。

- 使用 URLComponents(string:) 並以 https://eol.org/api/search/1.0.json 為基礎。
- 並用 URLQueryItem(name:value:) 來添加查詢條件。

可以根據不同的「搜索條件」建立不同的 EOLSearchAPIRequest 實例。
並使用相同的方法來處理這些請求,減少重複程式碼的需要。

3.Pages API

  • 這個API透過特定的「生物ID」來查詢更多的詳細資訊。
主要處理部分:

1. taxonConcept 包含了生物的詳細介紹。

2. dataObjects 陣列提供有關圖片的資訊。
- 這部分可能為空或不存在,所以最好將其設為選擇性數據。
- 如果有數據,它會包含圖片的URL(eolMediaURL)。

3. rightsHolder、agents 和 license 提供版權和來源資訊。
taxonConcept :最主要的資料結構,包含生物詳細資訊。
taxonConcepts 陣列:optional,會包含不同來源的分類訊息。通常,只需使用第一個結果。
dataObjects :用來儲存與圖像相關的資料。/ agents:版權和來源

3–1.解析 Pages API 回傳的結構建立

  • 針對所需的部分進行設置,後續將作為「詳細資料」項目顯示。

3–2.建立用於處理對「EOL Pages API」的請求

創建API請求從EOL Pages API獲取選定生物的詳細資訊,包括它的科學分類、相關圖像等。

屆時使用 EOLItemDetailAPIRequest,可以根據特定的「生物ID」構建請求URL,
並解析回傳的JSON數據為Swift中的結構體。

4.Hierarchy API

  • 由於出現錯誤,因此就先使用教材上給的提示來處理。
1. Hierarchy API 透過從之前的 Pages API 獲得的id,來查詢特定生物的分類詳情。

2. EX: https://eol.org/api/hierarchy_entries/1.0/7226994.json?language=en。

組合 URL,需要將基礎 URL("https://eol.org/api/hierarchy_entries/1.0/")與id和 ".json" 組合起來。
ancestors 陣列包含了各種分類等級(如界、門、綱等)的資訊。

4–1.解析 Hierarchy API 回傳的結構建立

1. EOLHierarchy 從 API 獲得的整體分類資訊,其中 ancestors 陣列包含每個 Ancestor 的具體資訊。

2. Ancestor 則用來描述每個Ancestor的學名和分類等級,在 App 中更容易地處理和展示這些資訊。

4–2.建立用於處理對「EOL Hierarchy API」的請求

1. 從 hierarchy_entries API 獲取「生物分類」的詳細資訊。

2. 使用 identifier 來指定要查詢的特定生物分類。

5.Image Request

  1. 圖片獲取:從EOL的API中,當您查詢某種生物時,如果該生物有可用的圖片,您可以透過獲取的資料中的 eolMediaURL 字段找到圖片的網址。
  2. 圖片轉換:您需要從這個網址下載圖片數據,然後利用 UIImage(data:) 方法把這些數據轉換成UIImage格式,以便在應用程式中顯示。
1. 圖片獲取:
- 從EOL的API中,當查詢某種生物時,如果該生物有可用的圖片,透過獲取的資料中的
eolMediaURL 找到圖片的網址。

2. 圖片轉換:
- 從這個網址下載圖片數據,然後利用 UIImage(data:) 把這些數據轉換成UIImage格式,
以便在 App 中顯示。

6.Issuing the API Requests

1. 通用請求方法:
- 使用泛型(generics)建立適用於各種API請求的通用方法,這樣就不必為每一種API
寫不同的請求方法了。

2. 設計協定與請求結構:
- 應該建立一個協定,讓各種API請求的結構體遵循這個協定。
(就像之前在APOD的例子中所做的,為每個API請求類型建立特定的請求結構。
  • 從搜尋功能開始
首先從搜尋功能開始,編寫一個通用函示來處理這類請求。
這樣就能迅速用搜尋結果填充 App 的主畫面表格。
  • 處理詳細數據請求
 - 在第二個畫面,可以在viewDidLoad()裡發送用於獲取詳細數據的請求。
- 由於已經建立了「請求協定」和「通用網路請求方法」,因此為其他API請求建立結構並
發送它們將會相當簡單。
  • 依次發送請求
1. Pages API 請求:
- 首先,使用 pages API 請求獲得生物的基本資訊。
- 包括學名(scientificName)、分類概念(taxonConcepts)、可能包含的圖片資訊(dataObjects)。

2. Hierarchy Entries API 請求:
- 接下來,利用從 taxonConcepts 中獲得的第一個元素的identifier,
通過 hierarchy entries API 請求該生物的更詳細分類資訊。

- 這將提供生物在生物分類學中的完整位置,如界、門、綱、目、科、屬等。

3. 圖片請求:
- 如果 dataObjects 中有圖片資訊(eolMediaURL),則對這個 URL 發送請求以獲取相應的圖片。
如果圖片可用,則在 App 中展示。

4. 版權和授權資訊的顯示:
- 顯示圖片的版權持有者,以及相關的授權資訊。
- 展示taxonConcepts陣列中第一個元素的nameAccordingTo資訊。
  • 更新分類詳細資訊。
ancestors 是一個包含多個Ancestor資訊的陣列,每個元素都包含有關生物分類的詳細信息。

.first(where: {$0.taxonRank == "kingdom"})
- 是尋找陣列中第一個「taxonRank」屬性等於「kingdom」的元素。

?.scientificName
- 則是嘗試取得找到的元素的「scientificName」屬性值,即學名。
如果沒有找到符合條件的元素,則結果為 nil。

也就是說目的是從 Ancestor 資訊中找到特定分類等級(如「界」)的元素,並取得其學名。
如果找不到,則不顯示任何信息。
  • 整理
使用了 async let 來並行處理分類資訊和圖片的加載請求,提高效率。
因為這兩個請求會同時執行,而不是一個接一個。當這兩個請求都完成後,再更新用戶界面。

--

--

wei Tsao 學習紀錄
彼得潘的 Swift iOS / Flutter App 開發教室

Hi ! 我是wei , 先前未接觸過程式開發設計,想藉此來記錄自己的學習歷程,以利培養自己的程式邏輯 :)