「想知道嗎?」- 使用 Enumeration 重構你的程式碼
讓我們使用 Swift 中的 Enumeration 這個強大的類型,來幫助我們的程式碼變得更好、更容易理解吧!
# 前言:
之前的一篇文章「Swift — 使用 Enumeration 重構您的程式碼.」中提到了一些使用 enum 重構程式碼的部分,但可能文章中說明得不夠詳細、不夠深入讓讀者明白 為什麼使用它 以及 怎麼使用它 。因此藉由好想工作室的「想知道嗎?」來重新整理了一份使用 Enumeration 重構程式碼的簡報,並且透過一些實際的範例比較前後的差異,可能讓聽眾能夠有比較深刻的體會。
因此也藉由這次的機會來重新發表一次使用 Enumeration 文章,希望能讓讀者有更深刻的體會。
# Enumeration 概念科普
# 定義 enum
簡稱 enum,又稱枚舉、列舉
你可以如下圖一樣,簡易的定義出一個 CompassPoint
的 enum
,其中有東、南、西、北,四種情況:
你也可以使用單行來定義所有情況,如下圖 Planet
的 enum
:
Enum 為了一組相關值定義了一個通用類型,使你能夠在程式碼中以類型安全的方式處理這些值。
⚠️ 每個枚舉都定義了一個新的類型,如同 Swift 中其他的類型一樣。名稱以大寫字母開頭,給枚舉類型起一個單數的而不是複數的名字,從而使得它們能顧名思義。
# 宣告 enum
你可以使用下面兩種方式來宣告 enum
實例:
當設置的類型為 enum
時,因為該類型是已知的,所以再次賦值時不需在編寫一次類型。這樣做可以使你在操作確定類型的 enum
時讓程式碼非常安全、易讀。
# 使用 Switch 匹配 enum 情況
使用 switch
語句匹配 enum
是一種很棒的用法,當你使用 switch
匹配 enum
時,因為 enum
的 case
是已知且有規範的,所以你能夠明確的可以對於每個 case
進行不同操作,而你當匹配所有情況時也不需加上 default
來處理例外情況(因為不會有例外)。
下面我們對 directionToHead
使用 switch
來匹配不同情況:
而當然你也可以只對於某個 case
做操作,其他情況一樣使用 default
來處理:
當我們已經覆蓋所有情況時,就不需要加上
default
的情況來處理沒涵蓋的情況。但假設我們今天沒有涵蓋所有情況時,我們還是必須要加上default
來處理剩餘的情況。
# RawValue
作為關聯值(Associated Values)的另一種選擇,Enum 情況可以用相同類型的原始值。你不需要顯式的給為每種情況都分配一個原始值。當你沒有分配時,Swift 將會自動為你分配值。
下面我們宣告一個 Int
類型的 enum
,下圖左邊我們為所有值指定其 rawValue
,右圖我們將第一個 case
指定 rawValue
為 1
,Swift 會自動分配其他情況的 rawValeu
與左圖相同。
而下圖每個 case
的 rawValue
也與上個圖相同。
若是 rawValue
之中設定其他值,其他 rawValue
則由他開始繼續往下加 1。
我們可以透過下列方式來訪問 rawValue
:
若 enum
類型為 String
時,其 rawValue
預設為 case
名稱:
# CaseIterable
提供所有情況的集合
對於某些 enum
來說,如果可以有一個集合包含了枚舉的所有情況是很棒的。而我們可以通過在 enum
名稱後面寫上 CaseIterable
如上圖,來使 enum
可迭代。
Swift 將所有 case
的集合為 enum
類型的 allCases
屬性。
# 由 rawValue 初始化
上面提到我們可以為 enum
的情況設置 rawValue
,因此我們也能透過 rawValue
來初始化為我們的 enum
情況。
⚠️ 而特別要注意的事情是它為一個可失敗初始化器( failable initializer ),因為不是所有的
rawValue
有相應的case
來初始化。因此會返回 Optional value 或是 nil,所以上面兩個常數類型皆為 Planet?。
# Enum 配合 Tag 使用
下面這邊我們使用一個情境來作為範例:
# 功能:
這是一個會開啟其他應用程式的 App,點選不同 icon 將會開啟不同應用程式
這邊最簡單的方式是你可以對於每個按鈕設定不同的 Action
,個別處理,在程式碼中可能像這樣:
而我們可以從 Received Actions 看對每個按鈕各自對應一個 Action:
而我們如果今天希望我們只要透過一個叫 openApp()
的函數,就能開啟不同應用程序該怎麼做呢?我們只要透過幾個步驟:
- 第一步: 將按鈕設置
tag
,由左到右,由上到下 1 ~ 6 的順序。
- 第二步:定義名為
App
的enum
,並且設定與上圖相應的rawValue
。
- 第三步:將按鈕連向同一個
IBAction
,因為我們需要按鈕的 tag 屬性,所以 sender 要選擇UIButton
。
完成之後,我們的 Received Actions 應該會長這樣,所有的按鈕都觸發同一個 Action。
接著我們會配合 enum
來使用它,首先我們會透過 button 的 tag
屬性,藉由 App
的可失敗初始化器來初始化 enum
情況,同時也透過 guard
語句來判斷初始化是否有值(不為 nil
)。
而當初始化有值之後,我們就能透過 switch
語句來對不同的情況進行不同的操作,因此我們可以很容易的在每個 case
下配置每個 App 的路徑,在最後我們將會藉由這個路徑開啟 App。
# 重構索引值(Index)
而在 iOS 開發中,常常我們想要知道某個東西被點選或是想要點選某樣東西時,可能都會透過 index 的方式來取用它或是改變它。因此如果我們能知道該 index 指的是什麼東西時,使用起來可能更加方便、快速。
這邊我們使用 SegementedControl
這個元件作為範例:
# 功能:
可以透過 SegementControl
來切換 免費 App 以及 付費 App 項目
因此我們與上面 tag 的方式雷同,只是因為我們可以透過 SegmentControl
中的 selectedSegmentIndex
來知道被選擇的項目,因此不需設置 tag
。
首先我們一樣定義一個名為 LeaderboardType
的 enum
:
接著當我們設定一個 Value Changed 的 IBAction
,當 SegmentedContol
值改變時,則會觸發這個方法,sender 記得也要選擇 UISegmentedContol
:
如此一來我們可以很容易的知道目前選擇到的 index 要做什麼操作,而不再是一個單純的 0、1 或是其他數字。你也能透過 allCases[index]
來取用情況。
想當然的,我們也能夠反向操作,我們可以透過 enum
產生我們的元件。這邊我們透過 LeaderboardType
的 allCases
的屬性產生 items
,並藉由這個 items
實例化 SegmentedControl
。
我們還能夠在 enum
中新增屬性,為每個情況配置不同值:
透過新增屬性可以讓我們操作起來更加的靈活,我們這邊透過 allCases 中美個項目的 title
來產生 items
,同樣的實例化 SegmentedControl
:
# 重構 TableView, CollectionView
相信到這邊大家都能猜出我們要來重構 IndexPath
的部分。因為往往我們在處理 TableView
和 CollectionView
時,一定常常碰到使用 indexPath
的地方,但如果這些數值能夠透過其他更直接的方式讓人理解,操作起來也會相對容易。
這邊一樣以 MOPCON 的 App 作為範例:
當一看到這個畫面,相信每個人對於畫面該怎麼切分區塊有不同的理解,可能有人覺得可以切分成 4 個區塊,但有人可能覺得切分成 2 區塊就足矣。正因為每個的區分的方式不同,因此如果該專案之後需要與人合作或是接手,新的開發人員可能還需要回頭去看畫面是如何被切分的。
因此,如果有一個地方能讓人參考畫面的整體架構的話,那麼會讓人理解的更快,並且開發起來也會相對快速。
接著我們將它遵循 CastIterable
的協議,因為我們需要獲取 allCases
:
我們可以先將 HomeSection.allCasse
存入到一個常數中使用,之後就不用每次都使用 guard
方式來判斷是否有值:
下面示範如何使用這個 sections
屬性,首先是 numberOfSections
方法:
接著我們示範 sizeForItemAt
的方法,我們可以透過 sections[index]
的方式將我們的 HomeSection
的 case
取出,之後透過 switch
語句來判斷是哪種情況,返回不同 CGSize
:
而為什麼要將 allcases
存到 viewController
中就如上面所述,這樣就不需要每次都要透過 guard
來判斷是否有值,而是直接取用 Array
中的值:
而我們也能透過這種方式產生我們的 sections
的內容,當你這樣做時程式也能夠正常運作,不需修改任何地方。因為我們每個地方都是在 switch
sections
中某個 enum
類型的情況,不會有任何關於 indexPath
問題,畫面會根據 HomeSection
的情況顯示不同的畫面內容:
# 轉換自定義類型
常常我們在處理一些資料時,如果可以把輸入的東西轉換成一個有規範的 enum
類型值是很有用的,下面我們示範一些在處理網路請求可以使用 enum
的狀況。
例如:我們在發送 request
時,有時候會需要設定請求的 httpMethod
,而這個值需要輸入一個 String
值。這時,我們就能夠定義一個 enum
來快速、安全地使用這個值:
如此一來我們我們在調用時只需要 HTTPMethod.get.rawValue
即可。
也能夠是定義 HTTPStatusCode
,透過 statusCode
知道請求的狀態:
你可能透定義一個 enum
類型遵循 Error
,來返回自定義的錯誤:
這些基本上都是讓某個數值和 enum
類型能夠互相對應、轉換,透過這些操作讓某個數值變得有意義。你也能夠將後端傳遞回來的資訊轉換為自定義的格式。
下面我們假設後端會回傳下面資料來判斷使用者性別,會回傳 { “sex”: “Male” }
或是 { “sex”: “Female” }
,我們可以根據這個資料內容定義一個名為 Sex
的 enum
:
將後端回傳的性別 String
值轉換成自定義的 Sex
類型值,如此一來我們也能對這個值進行安全的操作,也不用害怕資料錯改。
當然如果害怕後端可能會回傳給你例外的 String
值,你也可以額外定義一個 none
的 case
來處理值。
如果後端回傳給我們的 String
值無法初始化為 Sex
的情況,那我們就將它設為 Sex.none
,若有則正常初始化成 .male
或 .female
。
# 結論
“ 為什麼要如此多此一舉?🤔 ”
* 其一、沒有人知道你的 tag 到底是什麼
*其二、沒有人知道 index 是代表哪件事情
*其三、每個人劃分區塊( Section )的方式不同
根據區塊劃分的不同,所編寫的方式也會不同,因此如果我們有一套標準可以參考,那麼大家就能夠照著那套規範來開發畫面,之後有新的開發者加入也能夠很直接明白的知道每個區塊的定義是哪邊,而不用因為不知道區塊的 IndexPath
是多少而到處找來找去。
上面是沒有透過 enum
編寫的 sizeForItemAt
方法,當我們開發的時候還要去找 0, 1, 2, 3, 4 是代表哪幾個區塊,又或是有沒有 default
的狀況,而這種架構在區塊內容變多變少時很容易出錯,並且總是要加上 default
狀況。
而接下來我們透過 enum
方式來區分我們 section
的區塊,我們能夠很直觀地對於不同區塊進行調整,也沒有例外狀況需要處理,而也不會因為區塊變多變少導致畫面出錯。
可能在某些地方你可能會想設定 tag
或是 index
來進行某些操作,但對於我們來說它可能只是一個 Int
類型值,實際上也不了解這些數值的作用到底是什麼。但是,我們可能透過某個 enum
來實例化成一個情況,明白的表示出這個數值所代表的意思,如此一來也能讓一個普通的數值,轉換成有意義的值。
以下有幾個使用 enum
的優點:
- 定義簡單
- 類型安全、易讀、操作方便
- 不會有超出預期的狀況,若需要可以藉由
guard
方式提早退出 - 不會由無窮的情況出現,當涵蓋所有情況也不需要處理
default
- 架構可以藉由
enum
產生,可以彈性新增、修改內容 - 取代
Int
、Double
、String
、數字類型等等…,使用自定義類型取代
# 後記:
其實這篇講的落落長,其實就是一個核心概念:
“ 把數值轉換成一個有意義的程式碼名稱 ”
雖然這些用法實際上可能對於程式碼的運作沒有實際上的幫助,但藉由這些轉換能讓你在程式碼中操作這些值能夠更加直觀、快速並且操作起來相對安全了許多,透過將這些數值轉換成某個有意義類型的時,我們就能透過該類型以及情況來判斷及了解他的意思。本篇文章雖然是個簡單的基本概念,但還是希望能透過一些實際範例讓讀者體會到「 為什麼要使用它? 」。
最後也感謝看到最後的讀者們,如果這篇文章對你有幫助的話,也請點個拍手當作對我的支持及鼓勵,謝謝。