「想知道嗎?」- 使用 Enumeration 重構你的程式碼

讓我們使用 Swift 中的 Enumeration 這個強大的類型,來幫助我們的程式碼變得更好、更容易理解吧!

Jeremy Xue
Jeremy Xue ‘s Blog
16 min readJul 1, 2019

--

「 想知道嗎?」活動分享當天

# 前言:

之前的一篇文章「Swift — 使用 Enumeration 重構您的程式碼.」中提到了一些使用 enum 重構程式碼的部分,但可能文章中說明得不夠詳細、不夠深入讓讀者明白 為什麼使用它 以及 怎麼使用它 。因此藉由好想工作室的「想知道嗎?」來重新整理了一份使用 Enumeration 重構程式碼的簡報,並且透過一些實際的範例比較前後的差異,可能讓聽眾能夠有比較深刻的體會。

因此也藉由這次的機會來重新發表一次使用 Enumeration 文章,希望能讓讀者有更深刻的體會。

# Enumeration 概念科普

# 定義 enum

簡稱 enum,又稱枚舉、列舉

你可以如下圖一樣,簡易的定義出一個 CompassPointenum,其中有東、南、西、北,四種情況:

CompassPoint 的 enum

你也可以使用單行來定義所有情況,如下圖 Planetenum

使用單行定義 Planet 的enum

Enum 為了一組相關值定義了一個通用類型,使你能夠在程式碼中以類型安全的方式處理這些值。

⚠️ 每個枚舉都定義了一個新的類型,如同 Swift 中其他的類型一樣。名稱以大寫字母開頭,給枚舉類型起一個單數的而不是複數的名字,從而使得它們能顧名思義。

# 宣告 enum

你可以使用下面兩種方式來宣告 enum 實例:

當設置的類型為 enum 時,因為該類型是已知的,所以再次賦值時不需在編寫一次類型。這樣做可以使你在操作確定類型的 enum 時讓程式碼非常安全、易讀。

可以在 enum 類型中安全的操作值,不會有例外情況

# 使用 Switch 匹配 enum 情況

使用 switch 語句匹配 enum 是一種很棒的用法,當你使用 switch 匹配 enum 時,因為 enumcase 是已知且有規範的,所以你能夠明確的可以對於每個 case 進行不同操作,而你當匹配所有情況時也不需加上 default 來處理例外情況(因為不會有例外)。

下面我們對 directionToHead 使用 switch 來匹配不同情況:

對於不同 case,印出不同方位

而當然你也可以只對於某個 case 做操作,其他情況一樣使用 default 來處理:

針對 earth 情況,印出資訊

當我們已經覆蓋所有情況時,就不需要加上 default 的情況來處理沒涵蓋的情況。但假設我們今天沒有涵蓋所有情況時,我們還是必須要加上 default 來處理剩餘的情況。

# RawValue

作為關聯值(Associated Values)的另一種選擇,Enum 情況可以用相同類型的原始值。你不需要顯式的給為每種情況都分配一個原始值。當你沒有分配時,Swift 將會自動為你分配值。

下面我們宣告一個 Int 類型的 enum,下圖左邊我們為所有值指定其 rawValue ,右圖我們將第一個 case 指定 rawValue1 ,Swift 會自動分配其他情況的 rawValeu 與左圖相同。

兩者 rawValue 皆相同

而下圖每個 caserawValue 也與上個圖相同。

若是 rawValue 之中設定其他值,其他 rawValue 則由他開始繼續往下加 1。

rawValue 由上方有定義 rawValue 的 case 往下遞增

我們可以透過下列方式來訪問 rawValue

訪問 Planet 情況的 rawValue

enum 類型為 String 時,其 rawValue 預設為 case 名稱:

String 類型的 enum 預設 rawValue 為 case 本身的名稱

# CaseIterable

提供所有情況的集合

讓 CompassPoint 遵循 CaseIterable 協議

對於某些 enum 來說,如果可以有一個集合包含了枚舉的所有情況是很棒的。而我們可以通過在 enum 名稱後面寫上 CaseIterable 如上圖,來使 enum 可迭代。

Swift 將所有 case 的集合為 enum 類型的 allCases 屬性。

透過 CompassPoint 的 allCases 獲取所有情況

# 由 rawValue 初始化

上面提到我們可以為 enum 的情況設置 rawValue ,因此我們也能透過 rawValue 來初始化為我們的 enum 情況。

enum 的可失敗的初始化器
透過 rawValue 來初始化 enum 情況

⚠️ 而特別要注意的事情是它為一個可失敗初始化器( failable initializer ),因為不是所有的 rawValue 有相應的 case 來初始化。因此會返回 Optional value 或是 nil,所以上面兩個常數類型皆為 Planet?。

# Enum 配合 Tag 使用

下面這邊我們使用一個情境來作為範例:

# 功能:

這是一個會開啟其他應用程式的 App,點選不同 icon 將會開啟不同應用程式

這邊最簡單的方式是你可以對於每個按鈕設定不同的 Action ,個別處理,在程式碼中可能像這樣:

對不同按鈕設置不同 IBAction

而我們可以從 Received Actions 看對每個按鈕各自對應一個 Action:

而我們如果今天希望我們只要透過一個叫 openApp() 的函數,就能開啟不同應用程序該怎麼做呢?我們只要透過幾個步驟:

  • 第一步: 將按鈕設置 tag,由左到右,由上到下 1 ~ 6 的順序。
設置 Button 的 tag
  • 第二步:定義名為 Appenum ,並且設定與上圖相應的 rawValue
定義 enum 以及 rawValue
  • 第三步:將按鈕連向同一個 IBAction ,因為我們需要按鈕的 tag 屬性,所以 sender 要選擇 UIButton
不管哪個按鈕被點擊都會調用 openApp 方法

完成之後,我們的 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

首先我們一樣定義一個名為 LeaderboardTypeenum

定義 free 以及 paid 兩種 case

接著當我們設定一個 Value Changed 的 IBAction ,當 SegmentedContol 值改變時,則會觸發這個方法,sender 記得也要選擇 UISegmentedContol

透過 selectedSegmentIndex 判斷選擇項目是什麼

如此一來我們可以很容易的知道目前選擇到的 index 要做什麼操作,而不再是一個單純的 0、1 或是其他數字。你也能透過 allCases[index] 來取用情況。

想當然的,我們也能夠反向操作,我們可以透過 enum 產生我們的元件。這邊我們透過 LeaderboardTypeallCases 的屬性產生 items ,並藉由這個 items 實例化 SegmentedControl

透過 allCases 每個項目轉為字串來生成元件

我們還能夠在 enum 中新增屬性,為每個情況配置不同值:

透過新增屬性可以讓我們操作起來更加的靈活,我們這邊透過 allCases 中美個項目的 title 來產生 items ,同樣的實例化 SegmentedControl

透過 allCases 每個項目的 title 來生成元件

# 重構 TableView, CollectionView

相信到這邊大家都能猜出我們要來重構 IndexPath 的部分。因為往往我們在處理 TableViewCollectionView 時,一定常常碰到使用 indexPath 的地方,但如果這些數值能夠透過其他更直接的方式讓人理解,操作起來也會相對容易。

這邊一樣以 MOPCON 的 App 作為範例:

MOPCON APP 主畫面

當一看到這個畫面,相信每個人對於畫面該怎麼切分區塊有不同的理解,可能有人覺得可以切分成 4 個區塊,但有人可能覺得切分成 2 區塊就足矣。正因為每個的區分的方式不同,因此如果該專案之後需要與人合作或是接手,新的開發人員可能還需要回頭去看畫面是如何被切分的。

因此,如果有一個地方能讓人參考畫面的整體架構的話,那麼會讓人理解的更快,並且開發起來也會相對快速。

定義 Section 架構

接著我們將它遵循 CastIterable 的協議,因為我們需要獲取 allCases

我們可以先將 HomeSection.allCasse 存入到一個常數中使用,之後就不用每次都使用 guard 方式來判斷是否有值:

下面示範如何使用這個 sections 屬性,首先是 numberOfSections 方法:

透過 sections.count 返回 section 區塊數

接著我們示範 sizeForItemAt 的方法,我們可以透過 sections[index] 的方式將我們的 HomeSectioncase 取出,之後透過 switch 語句來判斷是哪種情況,返回不同 CGSize

而為什麼要將 allcases 存到 viewController 中就如上面所述,這樣就不需要每次都要透過 guard 來判斷是否有值,而是直接取用 Array 中的值:

而我們也能透過這種方式產生我們的 sections 的內容,當你這樣做時程式也能夠正常運作,不需修改任何地方。因為我們每個地方都是在 switch sections 中某個 enum 類型的情況,不會有任何關於 indexPath 問題,畫面會根據 HomeSection 的情況顯示不同的畫面內容:

# 轉換自定義類型

常常我們在處理一些資料時,如果可以把輸入的東西轉換成一個有規範的 enum 類型值是很有用的,下面我們示範一些在處理網路請求可以使用 enum 的狀況。

例如:我們在發送 request 時,有時候會需要設定請求的 httpMethod,而這個值需要輸入一個 String 值。這時,我們就能夠定義一個 enum 來快速、安全地使用這個值:

定義 HTTPMethod 的 enum

如此一來我們我們在調用時只需要 HTTPMethod.get.rawValue 即可。

也能夠是定義 HTTPStatusCode ,透過 statusCode 知道請求的狀態:

你可能透定義一個 enum 類型遵循 Error ,來返回自定義的錯誤:

自定義的網路錯誤 enum

這些基本上都是讓某個數值和 enum 類型能夠互相對應、轉換,透過這些操作讓某個數值變得有意義。你也能夠將後端傳遞回來的資訊轉換為自定義的格式。

下面我們假設後端會回傳下面資料來判斷使用者性別,會回傳 { “sex”: “Male” } 或是 { “sex”: “Female” } ,我們可以根據這個資料內容定義一個名為 Sexenum

將後端回傳的性別 String 值轉換成自定義的 Sex 類型值,如此一來我們也能對這個值進行安全的操作,也不用害怕資料錯改。

當然如果害怕後端可能會回傳給你例外的 String 值,你也可以額外定義一個 nonecase 來處理值。

如果後端回傳給我們的 String 值無法初始化為 Sex 的情況,那我們就將它設為 Sex.none ,若有則正常初始化成 .male.female

# 結論

“ 為什麼要如此多此一舉?🤔 ”

* 其一、沒有人知道你的 tag 到底是什麼

相較一下左邊與右邊的程式碼

*其二、沒有人知道 index 是代表哪件事情

選擇 0, 1 之後分別是要做什麼事情?

*其三、每個人劃分區塊( Section )的方式不同

根據區塊劃分的不同,所編寫的方式也會不同,因此如果我們有一套標準可以參考,那麼大家就能夠照著那套規範來開發畫面,之後有新的開發者加入也能夠很直接明白的知道每個區塊的定義是哪邊,而不用因為不知道區塊的 IndexPath 是多少而到處找來找去。

上面是沒有透過 enum 編寫的 sizeForItemAt 方法,當我們開發的時候還要去找 0, 1, 2, 3, 4 是代表哪幾個區塊,又或是有沒有 default 的狀況,而這種架構在區塊內容變多變少時很容易出錯,並且總是要加上 default 狀況。

而接下來我們透過 enum 方式來區分我們 section 的區塊,我們能夠很直觀地對於不同區塊進行調整,也沒有例外狀況需要處理,而也不會因為區塊變多變少導致畫面出錯。

可能在某些地方你可能會想設定 tag 或是 index 來進行某些操作,但對於我們來說它可能只是一個 Int 類型值,實際上也不了解這些數值的作用到底是什麼。但是,我們可能透過某個 enum 來實例化成一個情況,明白的表示出這個數值所代表的意思,如此一來也能讓一個普通的數值,轉換成有意義的值。

以下有幾個使用 enum 的優點:

  • 定義簡單
  • 類型安全、易讀、操作方便
  • 不會有超出預期的狀況,若需要可以藉由 guard 方式提早退出
  • 不會由無窮的情況出現,當涵蓋所有情況也不需要處理 default
  • 架構可以藉由 enum 產生,可以彈性新增、修改內容
  • 取代 IntDoubleString、數字類型等等…,使用自定義類型取代

# 後記:

其實這篇講的落落長,其實就是一個核心概念:

“ 把數值轉換成一個有意義的程式碼名稱 ”

雖然這些用法實際上可能對於程式碼的運作沒有實際上的幫助,但藉由這些轉換能讓你在程式碼中操作這些值能夠更加直觀、快速並且操作起來相對安全了許多,透過將這些數值轉換成某個有意義類型的時,我們就能透過該類型以及情況來判斷及了解他的意思。本篇文章雖然是個簡單的基本概念,但還是希望能透過一些實際範例讓讀者體會到「 為什麼要使用它? 」。

最後也感謝看到最後的讀者們,如果這篇文章對你有幫助的話,也請點個拍手當作對我的支持及鼓勵,謝謝。

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]