Swift. Delegation
讓我們理解 Delegation 是怎麼一回事吧!
▸ 前言:
委託(Delegation)是一種在 iOS 開發中常見的設計模式,而加上 iOS 又屬於協定導向程式設計(Protocol Oriented Programming,POP),常常會透過 dataSource 或 delegate 這種委託方式來處理畫面呈現以及事件傳遞,因此是一種必須要掌握的技術。
而這篇文章我會分享我認為的 Delegation 概念以及常見的問題,之後會再透過另一篇文章介紹更多不同的 Delegation 用法。
▸ 常見的迷思
我相信許多人第一次碰到 delegation(可能是 delegate)是在所謂的「資料回傳」或是「頁面傳值」。舉個最常見的例子,頁面互傳。如下圖:
當我們要從 PageA -> PageB 傳值時,我們可以透過在實例化 PageB 的物件之後,透過類似 PageB.value = 1
的方式直接修改 PageB 的資料。但是,反之,我們要從 PageB 傳值回 PageA 時,我們就無從下手了(因為不會在實例化一個 PageA 出來)。
那就用 Delegation 解決吧!?
其實我覺得這算是一種迷思。對我來說,因為 Delegation 只是解決傳值問題的其中一個方案,而不是絕對。你可以透過以下幾種常見的方式進行傳值:
- Closure
- Reference
- Delegation(也算一種 Reference)
- Notification、Observer
而另一個迷思則是「Delegation 一定要配合 Protocol 使用」,我認爲這部分以結果論來說這樣做比較好,但某部分來說不一定需要。我認為的 Delegation 就如同字面上的意思 — 委託。所以讓某個特定物件幫你處理某個特定事件時就算是委託,我們可以參考 wiki 上面的範例:
在委派模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委派給另一個對象來處理。
接著,我們先看常見的 Delegation 程式碼範例如下:
但其實我們可以不使用 protocol 就能達成相同操作,我們將 PageB
中的 delegate
的型別改為 PageA
即可:
由此可見,你應該可以了解 Delegation 本質上是使用 reference 的概念,而透過 protocol 的方式只是多了一個抽象的過程而已。
▸ 為什麼需要配合 Protocol
那為什麼通常 Delegation 都配合 Protocol 作為使用呢?從我們上面的範例看得出來,多了一個「抽象」的過程」,然而這有什麼用途呢?
這邊我們舉一個餐廳的範例,我們希望裡面有一個有煮飯的能力的員工,在不考慮任何情況下,我們的程式碼可能如下 :
這邊我們明確限制了員工的類型為 Employee
,而 Employee
中有 cook
方法讓我們能夠進行 Restaurant
中 cook
的操作。我們可以這樣解釋這段程式碼:
我們的餐廳可能有一個員工,並且我可以叫他煮飯(但這邊程式碼看起來比較像是「只要是員工,都會煮飯)
因為這邊我們明確標記員工的類型為 Employee
,所以,他無法被抽換為其他類別。但我們只是單純需要的只是一個有「煮飯的能力」的人,因此我們不應該限制他的類型(員工可以煮飯沒錯,但其他人也可能可以煮飯)。當你限制類型之後,之後再抽換類型會比較麻煩。
假設我們今天餐廳會有一個實習生,並且他也可以暫時代替員工幫忙煮飯,你的程式碼可能會這樣調整:
以上的兩個方案都可以解決這個問題,但你都需要修改原有的類型或是額外宣告內容。但我們想要的效果應該是「餐廳」有煮飯的功能,而這個功能只要是有「煮飯的能力」的人都可以(員工、實習生等等)。
左圖)
我們的餐廳可能有一個員工或實習生(都有/都沒有),我可以叫他們煮飯右圖)
我們的餐廳可能有一個員工,並且我可以叫他煮飯
(只要是員工或實習生,都會煮飯)
因此這邊我們可以透過 protocol 來幫我們實現這個操作。首先,我們先建立一個 Cookable
protocol,並且遵循此協議的物件必須要實現 cook
方法。(我們要求會煮飯的人,必須要有 cook 方法):
因此我們 Restaurant
中的程式碼可以調整下圖,我們刪除所有屬性,只有添加一個 someoneCanCook
變數,並且其類型為 Cookable
:
我們的餐廳可能有一個有煮飯能力的人,並且我可以叫他煮飯
接著我們就能夠任意抽換 someoneCanCook
中的物件(只要遵循 Cookable 協議),他們會執行相對應物件的 cook
方法的內容,並且不需要調整任何內容:
由此可見,假設我們把這個概念應用到常見的頁面間傳值上。你就知道為什麼每個範例的 Delegation 方式都會配合 Protocol 使用,因為只要單純描述是一個能夠接收資訊的人,而不是定義為某個明確的頁面或物件,如此一來,在抽換物件上或是進行重用時更為方便,增加程式碼的靈活度。
▸ 避免強引用循環
在我們使用 delegation 的時候,常常會出現 xxx.delegate = self
之類的程式碼,所以在使用上我們會飲用這些物件,所以必須考慮到可能有強引用循環的問題。這邊我們使用上面的範例,並在 PageA
中加入反初始化器:
接著我們運行以下的程式碼:
可以發現我們在將 pageA = nil
之後,我們呼叫 pageB.sendValueToPageA
依然能夠印出訊息,因為我們 pageA
實例還沒真正的被釋放掉(pageB.delegate
還持有對它的引用),所以在我們將 pageB.delegate
設為 nil 後,再次呼叫方法就不會印出任何訊息了(該 PageA
實例已被釋放),並且可以看見 PageA
中反初始化器所印出的訊息。
因此我們應該將 PageB
中的 delegate 屬性加上 weak
修飾,讓他對於 delegation 的物件保持弱引用來避免強引用循環。要做到這一點,我們必須在我們的協議之後加上 AnyObject
(或 class
,但將來可能棄用),讓此協議只能被 class 所遵循。
關於 AnyObject 或 class 的選擇,可以參考這篇文章:
https://forums.swift.org/t/class-only-protocols-class-vs-anyobject/11507/12
接著我們程式碼會變為以下這樣:
我們一樣執行與上述一樣的過程,但在我們 pageA = nil
的時候,該 PageA
實例也同時被釋放,之後無論怎麼呼叫 pageB.delegate
方法都不會再印出訊息。
▸ Delegation 對象
我們應該常常會看見 xxx.delegate = self
或是 xxx.dataSource = self
,但這也算是一種迷思,因為既然都是稱為「委託」模式了,我們也可以再將它委託給別人處理。舉個常見的 tableView.dataSource
的使用方式:
但其實這邊我們也可以額外寫一個物件,並讓他遵循 UITableViewDataSource
並且實作所需的方法即可。如此一來我們也可以得到相同的結果。
所以在使用 delegation 相關操作時,不一定是 delegate = self
,有時候你可以委託給一個物件,甚至使用透過 delegation 的方式交給另一個 delegation 去處理也是可行的。
所以,你可以根據情況使用最符合需求的委託方式,不一定都只能為 self。