在過去的七天,我們學會了 Swift 語法、學會了畫面排版,甚至為我們的 App 規劃了 MVC 架構。我們好像已經準備好要大展身手了。
但在我開始規劃「鬧鐘 App」的具體功能時,我發現了一個根本問題:
我的 App 至少會有兩個畫面:一個是「鬧鐘」,一個是「新增鬧鐘」。當我在「新增鬧鐘」畫面上點擊儲存後,「鬧鐘列表」畫面要如何知道這件事,並即時更新列表呢?
困惑點:「新增鬧鐘」畫面能不能直接呼叫「鬧鐘」畫面
AI 協作日誌
我是一個 iOS 新手。我的 App 有兩個 ViewController,A 和 B。A 呈現了 B。當 B 完成一個任務後(例如:點擊儲存按鈕),我該如何將 B 的資料傳回給 A,並通知 A 更新畫面?請用一個非技術性的簡單比喻。
AI 的回答 :
你好!這是一個在 iOS 開發中非常經典且重要的問題。作為新手,掌握這個概念對你未來的開發非常有幫助。
簡單來說,當 A 呈現 B 時,A 是 B 的「來源」或「父層」。B 不應該直接知道 A 的具體存在(這會讓程式碼耦合度太高),而是應該透過一個「約定」或「回呼機制」來通知 A。
最常用且專業的方法有兩種:Delegate (代理) 和 Closure (閉包)。以下我會用簡單的方式為你說明這兩種方法。
方法一:Delegate (代理模式)
你可以把 Delegate 想像成一份「合約」。ViewController B 定義了一份合約 (Protocol),裡面說明了「當我的任務完成時,我會執行這個合約上的方法」。而 ViewController A 則簽署這份合約,並實作合約裡的方法。
比喻:
- ViewController A (老闆):指派任務給 B。
- ViewController B (員工):執行任務。
- Protocol (工作回報合約):員工 B 身上帶著一份合約,規定任務完成後要如何向老闆回報。
- Delegate (代理人):老闆 A 自己就是那個接收回報的代理人。
操作步驟:
1. 在 ViewController B 中,定義合約 (Protocol) 並宣告一個代理人 (delegate)
// 記得加上註解,說明這個 Protocol 的用途 // ViewControllerBDelegate 是一個協定,用來將 ViewControllerB 的資料傳回 protocol ViewControllerBDelegate: AnyObject { // 當任務完成時,代理人需要實作這個方法 // withData 參數是用來傳遞資料的 func didFinishTask(withData data: String) } class ViewControllerB: UIViewController { // 宣告一個代理人屬性,型別是我們剛定義的協定 // 這裡使用 weak 關鍵字是為了防止「記憶體洩漏」(Retain Cycle) weak var delegate: ViewControllerBDelegate? // ... 你的其他程式碼 ... // 當儲存按鈕被點擊時 @IBAction func saveButtonTapped(_ sender: UIButton) { let dataToPassBack = "這是從 B 傳回的資料" // 透過代理人,呼叫合約上的方法,並把資料傳出去 delegate?.didFinishTask(withData: dataToPassBack) // 關閉自己,回到 A dismiss(animated: true, completion: nil) } }
2. 在 ViewController A 中,簽署並實作合約
class ViewControllerA: UIViewController { @IBOutlet weak var dataLabel: UILabel! // 當你要呈現 ViewController B 時 func presentB() { // 從 Storyboard 或用程式碼實例化 B let viewControllerB = storyboard?.instantiateViewController(withIdentifier: "ViewControllerB") as! ViewControllerB // 告訴 B:「我就是你的代理人!」 viewControllerB.delegate = self // 呈現 B present(viewControllerB, animated: true, completion: nil) } } // 透過 extension 讓程式碼更清晰 // 讓 ViewControllerA 遵守 (Conform to) ViewControllerBDelegate 這個協定 extension ViewControllerA: ViewControllerBDelegate { // 實作協定中定義的方法 func didFinishTask(withData data: String) { // 在這裡,你就能收到從 B 傳回來的資料 print("收到了來自 B 的資料: \(data)") // 更新畫面上的 Label // 重要:所有 UI 的更新都必須在主執行緒 (Main Thread) 上完成 DispatchQueue.main.async { self.dataLabel.text = data } } }
方法二:Closure (閉包 / 回呼函式)
Closure 是一種更現代、更輕量級的方式。你可以把它想像成 A 交給 B 一張「待辦紙條」。這張紙條上寫著:「當你完成任務後,請執行這張紙條上的指令」。
比喻:
- ViewController A (媽媽):請兒子 B 去買東西。
- ViewController B (兒子):去買東西。
- Closure (待辦紙條):媽媽在出門前,寫了一張紙條給兒子:「買完東西後,把東西放到冰箱裡(這就是紙條上的指令)」。兒子買完東西回來,看到紙條,就把東西放進冰箱。
操作步驟:
1. 在 ViewController B 中,定義一個 Closure 屬性
class ViewControllerB: UIViewController { // 宣告一個 closure 屬性 // 它的型別是一個函式,這個函式接收一個 String,並且沒有回傳值 (Void) // `?` 表示這個 closure 是可選的 (optional) var onCompletion: ((String) -> Void)? // ... 你的其他程式碼 ... @IBAction func saveButtonTapped(_ sender: UIButton) { let dataToPassBack = "這是從 B 透過 Closure 傳回的" // 如果這個 closure 存在,就執行它,並把資料當作參數傳入 onCompletion?(dataToPassBack) // 關閉自己 dismiss(animated: true, completion: nil) } }
2. 在 ViewController A 中,實作 Closure 的內容
class ViewControllerA: UIViewController { @IBOutlet weak var dataLabel: UILabel! func presentB() { let viewControllerB = storyboard?.instantiateViewController(withIdentifier: "ViewControllerB") as! ViewControllerB // 在呈現 B 之前,把「待辦紙條」交給它 // 這段 { [weak self] data in ... } 就是 closure 的實作內容 viewControllerB.onCompletion = { [weak self] data in // [weak self] 是為了防止記憶體洩漏 guard let self = self else { return } print("透過 Closure 收到了資料: \(data)") // 更新 UI DispatchQueue.main.async { self.dataLabel.text = data } } present(viewControllerB, animated: true, completion: nil) } }
給新手的建議:
兩種方法都非常重要,但我建議你先從 Delegate 模式學起。
因為 Delegate 是 Apple 官方框架(如
UITableViewDelegate
)中大量使用的模式,理解它有助於你理解整個 iOS SDK 的設計哲學。當你完全掌握 Delegate 後,再學習使用 Closure,你會發現它在處理簡單回呼時是多麼方便。希望這個說明對你有幫助!
特性 | Delegate (代理模式) | Closure (閉包) |
---|---|---|
結構 | 較為正式、嚴謹,需要定義 Protocol 。 |
較為輕量、簡潔,直接定義函式型別的屬性。 |
耦合度 | 低耦合,B 完全不知道 A 是誰,只認得合約。 | 低耦合,B 不知道是誰寫的紙條,只負責執行。 |
程式碼分散 | 實作邏輯會寫在 extension 裡,與觸發點分開。 |
回呼邏輯直接寫在呼叫 present 的地方,上下文更清晰。 |
適用情境 | 當 B 需要回報多種不同的事件時(例如:儲存成功、取消、刪除),用 Delegate 可以定義多個函式,結構較清晰。 | 當 B 只需要回報單一完成事件時,用 Closure 非常方便快速。 |
今天我們沒有寫任何專案程式碼,但卻學到了比程式碼更重要的 App 內部溝通
但在我們開始動手打造鬧鐘之前,還需要準備最後一樣東西:一個可靠的倉庫,用來存放我們建立的所有鬧鐘。
明天將介紹一個強大又好用的 iOS 資料庫:RealmSwift,為我們的鬧鐘實戰做好最後的準備!