利用 delegate 讓不同元件溝通,以資料回傳到前一頁 controller 為例
開發 iOS App 時,我們時常利用 delegate 的技巧讓不同元件溝通。它可以運用在很多地方,比方將資料回傳到前一頁的 controller 是最常見的例子。
以下圖為例,點選 New Event 頁面的 Repeat Cell 跳到下一頁,在 Repeat 頁面選擇重覆的頻率後,SelectFrequencyTableViewController 將返回前一頁並將選擇的頻率告訴 EditEventTableViewController。
在之前的文章我們以選擇最愛動物為例介紹了將資料回傳到前一頁 controller 的問題,接下來我們將改用更好的解法,利用 delegate 將資料回傳。
宣告 protocol 型別 & property delegate
之前的問題出在 property favoriteAnimalViewController 的型別為 FavoriteAnimalViewController,因此不夠彈性,只能儲存 FavoriteAnimalViewController,容不下其它的 controller。
所以我們將它改名為 delegate,並將型別改成自訂的 protocol SelectAnimalViewControllerDelegate。
protocol SelectAnimalViewControllerDelegate: AnyObject {
}
class SelectAnimalViewController: UIViewController {
weak var delegate: SelectAnimalViewControllerDelegate?
當變數的型別是 protocol 時,它會彈性許多,比方 FavoriteAnimalViewController & PsychologicalTestViewController 都能當 SelectAnimalViewController 的 delegate,只要它們遵從 protocol SelectAnimalViewControllerDelegate。
class FavoriteAnimalViewController: SelectAnimalViewControllerDelegate
class PsychologicalTestViewController: SelectAnimalViewControllerDelegate
值得注意的,剛剛宣告 protocol 型別 & property delegate 時,有 2 個特別的地方,protocol 冒號後的 AnyObject 跟 delegate 被宣告為 weak,相關說明可參考以下連結。
為什麼要叫 delegate
也許有人好奇,為什麼 property 名字要叫 delegate,而且 protocol 的名稱是原本的型別名稱加 Delegate ?
其實你要取別的名字也可以,只是開發 iOS App 時我們習慣取名 delegate,這樣可以跟 iOS SDK 原本的寫法一致,比方 UITableView & UIScrollView 都有 delegate,型別分別是 UITableViewDelegate & UIScrollViewDelegate。
delegate 的中文是代理人的意思,在剛剛選擇動物的例子可以想成 SelectAnimalViewController 有個無怨無悔,不求回報幫它做事的代理人。(或是想成我們比較熟悉的工具人)
當使用者在 SelectAnimalViewController 的畫面選完動物後,要依據選擇的動物發生什麼事呢 ?
SelectAnimalViewController 將這個重責大任交給代理人(delegate)決定,由代理人決定要做什麼。若以剛剛的 App 為例,我們希望能顯示動物的圖片,而 delegate 的型別宣告為 protocol 正是實現此功能的關鍵。
宣告 protocol 的 function
我們可在 protocol 裡宣告使用者已選擇動物的 function,然後讓自訂的某某型別遵從 protocol 並定義 function,由它來決定選擇動物後要做的事情。
protocol SelectAnimalViewControllerDelegate {
func selectAnimalViewController(_ controller: SelectAnimalViewController, didSelect animal: Animal)
}
我們宣告 function selectAnimalViewController(_ controller: SelectAnimalViewController, didSelect animal: Animal),在第二個參數animal 傳入選擇的動物。
也許有人覺得 function 的名字有點奇怪,其實這是模仿 iOS SDK 的寫法,類似以下 UIImagePickerControllerDelegate & UITableViewDelegate 的 function。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
從 iOS SDK 的例子,我們看到當元件有 delegate 時,delegate 的 function 通常有以下規則:
- function 名稱習慣取名元件的名稱
- 參數有兩個
(1) 元件自己。
讓 delegate 知道呼叫此 function 的元件是誰,包含此參數的好處可參考以下連結。
(2) 傳送的資料
因此 UIImagePickerController 的 delegate function imagePickerController(_:didFinishPickingMediaWithInfo:) 特徵如下:
- function 名字是 imagePickerController
- 第一個參數 picker 是 UIImagePickerController
- 第二個參數 info 是傳送的資料,使用者選擇的照片在 info 裡。
因此我們模仿剛剛的規則在 protocol SelectAnimalViewControllerDelegate 宣告已選擇動物的 function。
func selectAnimalViewController(_ controller: SelectAnimalViewController, didSelect animal: Animal)
它的特徵如下
- function 名字是 selectAnimalViewController
- 第一個參數 controller 是 SelectAnimalViewController
- 第二個參數 animal 是傳送的資料,在此為使用者選擇的動物。
通常 delegate protocol 裡宣告的 function 代表發生的事情,然後 delegate 可決定此時要做什麼,有興趣的朋友可參考以下 iOS SDK 的例子
- AVSpeechSynthesizer delegate 的 function
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance)
synthesizer 講完話,delegate 可定義 function 決定講完話要做什麼,比方開始播音樂。
- UIScrollView delegate 的 function
func scrollViewDidScroll(_ scrollView: UIScrollView)
scroll view 的畫面被捲動,delegate 可定義 function 決定畫面捲動時要做什麼,比方判斷捲動到畫面最下方時抓取更多的資料。
呼叫 delegate 的 function
使用者點選動物的 button 後,呼叫 delegate 的 selectAnimalViewController(_:didSelect:),告訴 delegate 使用者選了什麼動物。
@IBAction func selectAnimal(_ sender: UIButton) {
switch sender.currentTitle {
case "🐈":
delegate?.selectAnimalViewController(self, didSelect: .cat)
case "🐠":
delegate?.selectAnimalViewController(self, didSelect: .fish)
case "🦮":
delegate?.selectAnimalViewController(self, didSelect: .dog)
default:
break
}
navigationController?.popViewController(animated: true)
}
遵從 protocol & 定義 protocol 的 function
FavoriteAnimalViewController 必須遵從 protocol SelectAnimalViewControllerDelegate,才能當 SelectAnimalViewController 的 delegate,控制選了動物後顯示照片。
extension FavoriteAnimalViewController: SelectAnimalViewControllerDelegate {
func selectAnimalViewController(_ controller: SelectAnimalViewController, didSelect animal: Animal) {
animalImageView.image = UIImage(named: animal.rawValue)
}
}
設定 delegate
讓 FavoriteAnimalViewController 成為 SelectAnimalViewController 的 delegate,由 FavoriteAnimalViewController 決定選了動物後要顯示照片。
設定 delegate 有以下三種方法。
- 透過 segue 切換頁面時,在 IBSegueAction 設定 delegate。
@IBSegueAction func showSelectAnimal(_ coder: NSCoder) -> SelectAnimalViewController? {
let selectAnimalViewController = SelectAnimalViewController(coder: coder)
selectAnimalViewController?.delegate = self
return selectAnimalViewController
}
- 透過 segue 切換頁面時,在 prepare 裡設定 delegate。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let selectAnimalViewController = segue.destination as? SelectAnimalViewController
selectAnimalViewController?.delegate = self
}
- 不透過 segue,從程式生成 controller & 切換頁面。
if let selectAnimalViewController = storyboard?.instantiateViewController(withIdentifier: "\(SelectAnimalViewController.self)") as? SelectAnimalViewController {
selectAnimalViewController.delegate = self
show(selectAnimalViewController, sender: nil)
}
大功告成,現在我們透過 delegate 的方法將 SelectAnimalViewController 畫面選擇的動物傳給 FavoriteAnimalViewController。
透過下圖分割畫面並排 FavoriteAnimalViewController & SelectAnimalViewController,我們可以更容易理解剛剛介紹的 delegate 概念。
接著讓我們加入另一個 PsychologicalTestViewController 當 delegate,證明 delegate 方法帶給我們的彈性。任何 controller 只要有心,都可以當 delegate !
加入 PsychologicalTestViewController 當 delegate
PsychologicalTestViewController 將依據選擇的動物顯示心理測驗的結果,從動物算出你是什麼樣的人。
class PsychologicalTestViewController: UIViewController {
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBSegueAction func showSelectAnimal(_ coder: NSCoder) -> SelectAnimalViewController? {
let selectAnimalViewController = SelectAnimalViewController(coder: coder)
selectAnimalViewController?.delegate = self
return selectAnimalViewController
}
}
extension PsychologicalTestViewController: SelectAnimalViewControllerDelegate {
func selectAnimalViewController(_ controller: SelectAnimalViewController, didSelect animal: Animal) {
switch animal {
case .cat:
label.text = "喜歡 cat 的人都很可愛"
case .fish:
label.text = "喜歡 fish 的人都很有趣"
case .dog:
label.text = "喜歡 dog 的人都很聰明"
}
}
}
範例連結
其它範例: 自訂 delegate — Develop in Swift Data Collections 重點整理
雖然我們剛剛看到的 delegate 例子是讓兩個 controller 溝通,不過此概念可以運用在任何元件,不一定是 controller。有興趣的可再研究以下 Apple 電子書兩個 delegate 溝通的範例,其中一個是 table view controller 當 table view cell 的 delegate。
自訂 delegate 讓不同元件溝通的七個步驟
理解自訂 delegate 的原理後,以後只要記得以下七個步驟,即可快速實現讓不同元件溝通的功能。
以下我們假設有兩個 controller,FirstViewController 是 SecondViewController 之前的畫面,我們想將 FirstViewController 設為 SecondViewController 的 delegate,那麼實作時可分為以下 7 個步驟:
在 SecondViewController.swift
- 宣告 protocol
宣告 protocol SecondViewControllerDelegate。
protocol SecondViewControllerDelegate: AnyObject {
}
2. 宣告 protocol 的 function
宣告 function secondViewController(_:didSelect),假設發生的事情是使用者選了某個語言。
protocol SecondViewControllerDelegate: AnyObject {
func secondViewController(_ controller: SecondViewController, didSelect language: String)
}
3. 宣告 property delegate
宣告 property delegate。(ps: 依據情況判斷是否須加 weak,細節可參考前面的說明)
class SecondViewController {
weak var delegate: SecondViewControllerDelegate?
}
4. 呼叫 delegate 的 function
比方點選台灣的 button 後,呼叫 delegate 的 function。
@IBAction func selectLanguageTW(_ sender: UIButton) {
delegate?.secondViewController(self, didSelect: "tw")
}
在 FirstViewController.swift
5. 遵從 protocol
FirstViewController 必須遵從 protocol SecondViewControllerDelegate 才能當 SecondViewController 的 delegate。
extension FirstViewController: SecondViewControllerDelegate {
}
6. 定義 protocol 的 function
定義 function secondViewController(_:didSelect),從參數 language 取得使用者選擇的語言,決定這時候要做什麼。
extension FirstViewController: SecondViewControllerDelegate {
func secondViewController(_ controller: SecondViewController, didSelect language: String) {
print(language)
}
}
7. 設定 delegate
設定 delegate 有很多方法,以剛剛的 FirstViewController & SecondViewController 為例,若想讓 FirstViewController 成為 SecondViewController 的 delegate ,我們有三種方法。
- 透過 segue 切換頁面時,在 IBSegueAction 設定 delegate。
@IBSegueAction func showSecondViewController(_ coder: NSCoder) -> SecondViewController? {
let secondViewController = SecondViewController(coder: coder)
secondViewController?.delegate = self
}
- 透過 segue 切換頁面時,在 prepare 裡設定 delegate。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let secondViewController = segue.destination as? SecondViewController
secondViewController?.delegate = self
}
- 不透過 segue,從程式生成 controller 切換頁面。
if let secondViewController = storyboard?.instantiateViewController(withIdentifier: "\(SecondViewController.self)") as? SecondViewController {
secondViewController.delegate = self
show(secondViewController, sender: nil)
}
One more thing,定義 controller 的 init 設定 delegate
作業練習
利用 delegate,模仿實現 Setting App 自動鎖定時間設定的功能。只須做到畫面上顯示選取的時間,不須做到螢幕鎖定的功能。