18.%d Xcode Study — Passing Data using Delegate/Closure

春麗 S.T.E.M.
12 min readAug 24, 2021

--

目錄

⦿ 傳值回顧
⦿ Delegate 傳值基礎設置
⦿ Delegate、protocol與反向傳值
⦿ 再看 Delegate
⦿ closure 與反向傳值

傳值回顧

前一篇文章中,我們使用了更不一樣的做法來傳值,KVC 與 KVO,KVC 是利用繼承 NSObject 的 class 生成的物件,保有 NSKeyValueCoding 的特性去傳值。

在 KVC 中,其實我們可以把它看成 property 傳值的延伸,不過既然是 KVC,就有特殊取值設定的方式;KVO 則是在 KVC 物件基礎,加上 Listener,所以當物件的值改變時,會跟著改變一些其他東西,無論是 property,或是隨 property 變動的 UI。

這篇文章中,我們再來看看其他傳值方式,Delegate 與 Closure。

繼續閱讀|回目錄

Delegate 傳值基礎設置

在 iOS 開發中,我們常使用 TableView,所以也可以說常使用 UITableViewDataSource,DataSource 就是 Delegation Pattern(委任模式)的實際應用。

當一個 VC conform UITableViewDataSource,就可以使用下方的程式碼:

    func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return countries.count
}

func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default,
reuseIdentifier: "Cell")
let country = countries[indexPath.row]
cell.textLabel?.text = "\(country.emoji) \(innerBlank)\(country.dial_code)"

return cell
}

第一個 function 表示,顯示的 Table 有多少 Row,藉由回傳資料有多少個 Set 來作為 Row 的數量(Int)。

第二個 function 表示,Table 要顯示的 Cell 的 ID 是 Cell,每個 Cell 透過預設的 style 去設定它的標題,最後回傳這個 Cell。

當 VC conform 這個 DataSource,就可藉由這基本兩個 function 設定欲顯示的 Table,於是程式碼就會少很多。軟體工程師設計程式碼的其中一個原則就是減少重複,而這邊要特別注意的是 DataSource 是一個 protocol。

因為 conform 這個 protocol,所以 VC 可以代替 Table 做一些事,在這裡是設定它的顯示資料。而 Delegation Pattern 在 iOS 關乎另一個重要的功能—— Passing Data(傳值)。

我們先看到下面的基本設置:

左邊 Gif 圖是呈現的結果,在第一頁按下選擇鍵,會跳出第二頁讓使用者選擇性別,女 / 男 / 無,每個性別會跟著一張 Avatar,若不選擇回到第一頁,第一頁什麼都不會變。

選擇每張圖都會跟著不同的背景色(黃 / 橘 / 綠)帶回第一頁,當然,性別欄跟 ImageView 也會帶回這三張不同的 image

以上是頁面安排的基本,接著看到 ViewController(第一頁)的程式碼:

@IBAction func choosingBtnTapped(_ sender: Any) {
let vc = storyboard?.instantiateViewController(withIdentifier: "\(MyTableViewController.self)") as! MyTableViewController
vc.delegate = self

present(vc, animated: true, completion: nil)

}

如往常般,從 Storyboard 中產生 VC,接著 show 出 VC,暫時先忽略 vc.delegate = self,這是 Delegation Pattern 的核心。

再看到 MyTableViewController(第二頁)的程式碼:

@IBAction func firstBtnTapped(_ sender: Any) {
dismiss(animated: true, completion: nil)
}

@IBAction func secondBtnTapped(_ sender: Any) {
dismiss(animated: true, completion: nil)
}

@IBAction func thirdBtnTapped(_ sender: Any) {
dismiss(animated: true, completion: nil)
}

在這邊有一個小 trick,我們可以對照前段的 Storyboard 設置,ImageView 是方便擺設圖片的 UIElement,若希望點擊圖片作動(action),可以使用動作偵測(Tap Gesture),但也可以在 ImageView(image)上方放上相同大小的 Button,接著再將 Button 的顏色設置為 .clear,這時就可以穿透 Button 看到 image,我們點擊 image 也等於點擊 Button。

所以這些 IBAction 就是從 Button 拉出來的,而在過去的文章中,我們學過 Outlet Collection,同樣的方法也可以應用在 Action 中,我們若將所有的 Button Action 放在一起就會形成如下:

    @IBAction func btnTapped(_ sender: UIButton) {
// MARK: - delegation pattern
let userData = userDatas[sender.tag]
delegate.didChoosing(image: userData.avatar,
gender: userData.gender,
color: userData.customColor)

dismiss(animated: true, completion: nil)
}

同樣地,我們只先注意 dismiss,在按下 Button 就會解散回到上一頁;但也要注意 Storyboard 中,Button 的 tag,這個 tag 也如同 Outlet Collection 中,我們用來區分不同 Outlet 的編號

繼續閱讀|回目錄

Delegate、protocol 與反向傳值

在 Delegation Pattern 中的一個特性是,當這個頁面還沒產生時,仍可以透過一個接口 / 介面產生連結。

在 iOS 開發中所謂協定導向就是用來作為接口 / 介面的方式,我們先設置一個 protocol(接口)如下:

protocol AvatarChoosingDelegate {
func didChoosing(image: UIImage, gender: String, color: UIColor)
}

這個接口叫做頭像選擇的代理,要實作一個名稱叫已選擇的 function,裡面要傳遞的 property 為 image、gender、color,型別分別為 UIImage、String、UIColor,這些 property 就是要傳回第一頁,並設定第一頁的 UIElement 的資訊。

還記得前面說到第二頁尚未產生,這即是說,第一頁要代理第二頁完成這個設定(實作 function),我們只要著重在接口兩邊的資料傳遞,在第二頁先宣告一個 property 叫做 delegate 如下:

var delegate: AvatarChoosingDelegate!

下面則是接口傳遞資料:

在選擇 Avatar 後,透過呼叫 delegate 的 function 將資料放進去,接著回到上一頁。

而前面說過,這個設定的 function 還未實作,所以回到 ViewController 中,加上下面這個 extension:

extension ViewController: AvatarChoosingDelegate {  func didChoosing(image: UIImage, gender: String, color: UIColor) {    avatarImageView.image = image
genderLabel.text = gender
view.backgroundColor = color
}}

由於接口將資料傳遞過來,所以在這邊就可以使用資料設定 UI。

但是別忘了!我們回到前面,在第二頁剛剛產生,我們需要將第二頁的 delegate 設定為 self。

如此!透過 Delegate 傳值就完成了!

繼續閱讀|回目錄

再看 Delegate

經過前面,我們知道 Delegate 是基於 Protocol 下的一種使用方法,而 UITableViewDataSource 也是如此,當 tableView 的 dataSource 是 VC,就可以將程式碼的權責分開,當 VC conform 此 Protocol,就可對 tableView 提供 dataSource。

前面的 tableView numberOfRowsInSection 與 tableView cellForRowAt 即是如此,將資料有多少個 Set 提供給前者,再將各個 cell 欲呈現什麼資料項提供給後者,也是 tableView 的基礎

在 cellForRowAt,我們最常看到一段程式碼為 let cell = tableView.dequeueReusableCell(withIdentifier: “myCellIdentifier”, for: indexPath),這是為了在產生 cell 時不造成記憶體的浪費,使用 dequeue 讓產生過現在不出現在畫面上的 cell 是可以重用的,當然,此時點擊或 loading 的顯示內容就成了 issue,不過這邊不提。

在傳值中,我們可以這麼理解,如果(第二頁)要傳值給先於我產生的 View(第一頁),在還沒產生的時候,這個 View 要替我做什麼?例如將從這邊來的 dataSource 用以設定 View 本身。

所以 View 只要 conform protocol 去實作 function;而則是宣告 delegate 為這個 protocol(protocol 可以做為型別來使用),當要傳遞資料時就會觸發傳遞資料的方法,由於 View 代替做了這件事,所以回到上一頁時,View 的 Appearance 也跟著改變了。

這就是 Delegation Pattern 傳值的方式。

繼續閱讀|回目錄

closure 與反向傳值

要使用閉包傳值,我們要先知道閉包如何宣告,閉包就是匿名函式,也是一個執行區塊(Block)。

所以得先知道閉包要傳遞什麼,在第二頁的地方,我們會像這樣宣告:

var choosingCompletion: ( (UserData) -> Void )?

這個 UserData 是我們要傳過去的資料,也就是閉包的參數,閉包的回傳值是 Void,並且這個閉包必須是 Optional 的。

而當選擇鍵按下後,程式碼如下:

@IBAction func btnTapped(_ sender: UIButton) {        choosingCompletion?(userDatas[sender.tag])        
dismiss(animated: true, completion: nil)
}

必須代入參數。

在第一頁的地方,程式碼如下:

@IBAction func chooseBtnTapped(_ sender: Any) {
guard let vc = storyboard?.instantiateViewController(withIdentifier: "\(MyTableViewController.self)") as? MyTableViewController else { return }
vc.choosingCompletion = { [weak self] userData in
self?.avatarImageView.image = userData.avatar
self?.genderLabel.text = userData.gender
self?.view.backgroundColor = userData.customColor
}
present(controller, animated: true, completion: nil)
}

實際上,你必須了解,雖然 vc.choosingCompletion 寫在這裡,但它卻是在第二頁選擇後才觸發。

最終結果如下:

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

附上 GitHub:

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet