###### tags: `iOS` `Swift` # 頁面跳轉的大小事 頁面跳轉在App中是相當重要的環節,在iOS中大致分成兩種呈現方式 其他的方式是衍生出來或動畫效果 - Push : 頁面有順序,系統會將前面頁面放入堆疊,搭配 Navigation Controller 才可使用 - Present : 如同開啟新頁面,與上一畫面沒有順序關聯 ## 跳轉執行種類 跳轉的執行方式也分為兩種 - Segue 跳轉 : 在 Storyboard 中設定 Segue,分成有無程式碼 - 無程式 : 設定 Triggered Segues 把 action 拖曳至下一頁面,並設定呈現方式 - 有程式 : 用 performSegue 函式來執行預先定義好的 Segue - Storyboard ID 跳轉 : 在Storyboard中設定Storyboard ID,用程式碼指定要跳轉的 ID ==額外補充 Xib檔、Storyboard檔的區別,因為兩者的跳轉方式不同== | 特性 | Xib | Storyboard | |:------:|:-----------:|:-----------:| | 用途 | 只顯示一個畫面 | 顯示多個畫面 | | 維護難易度 | 檔案較小,多人開發時,較容易修改不造成衝突 | 檔案較大,多人開發時,不容易修改容易造成衝突 | | 初始化方式 | 透過 nib 初始化 | 先使用 nib 指定 Storyboard, 再使用 Storyboard ID 進行初始化 | | 頁面關聯性 | 無法看出頁面之間的關聯性 | 可透過頁面之間的連線看出各自的關係 | | 跳轉方式 | 需透過 Storyboard ID 才可跳轉頁面 | Segue、Storyboard ID 皆可跳轉頁面 | ## Segue 跳轉 使用 Segue 通常是希望不打程式碼來進行跳轉,但是過多跳轉會很複雜,導致不便管理 所以可以用程式碼的方式進行 Segue (即 performSegue)。 無程式碼就是省略了自己打 performSegue 的步驟,變成系統自己執行 performSegue 系統執行時 withIdentifier 會抓取 Storyboard Segue Identifier (沒設定會變成""空字串) sender 為設定 Segue 時綁定的元件。 ``` //a為Segue ID (類型為String),b為發送執行需求的元件(即被綁定的元件) (類型為Any?) performSegue(withIdentifier: a, sender: b) ``` 以下為有程式碼、無程式碼的區別 - 無程式碼:綁特定元件按壓執行、簡單方便、跳轉多不便管理 - 有程式碼:綁ViewController程式碼執行、多一步驟、跳轉多方便管理 若希望在跳至其他頁面前做事前處理 (例如:傳遞資料),可使用 [prepare](https://medium.com/彼得潘的-swift-ios-app-開發教室/簡易說明xcode中的資料傳遞-使用prepare傳遞property的方式-a5666ca36114) (Segue 獨有的週期) 利用 segue.identifier 判斷此次跳轉的 Segue 是哪一個,就可以進行個別的工作。 :::success 以下為操作方式,由於沒有設定 Navigation Controller(下面章節有教),所以皆為 Present ::: ### Segue - 無程式碼 點選要執行跳轉的元件,按下右鍵,將 action 拖曳至下一畫面,並選擇呈現方式 ![](https://hackmd.io/_uploads/ByDT9wAEn.png) 完成後會在兩個 ViewController 之間產生一條連接線,這樣就完成跳轉囉! ![](https://hackmd.io/_uploads/rJKAcP0Eh.png) ### Segue - 有程式碼 點 ViewController (上方三個圖示最左邊) 按右鍵,將 manual 拖曳至下一畫面,並選呈現方式 ![](https://hackmd.io/_uploads/Bk0givCV2.png) 兩個 ViewController 間產生一條連接線,點擊連接線,可以看到它對應到整個 ViewController ![](https://hackmd.io/_uploads/BkVzswCEn.png) 點擊連接線,設定 Storyboard Segue Identifier 為 nextSegue ![](https://hackmd.io/_uploads/SJs7iw0N2.png) 輸入程式碼,設定 Button 點擊事件,performSegue 執行指定的 ID 跳轉 prepare 為執行 Segue 後會進入的週期,在這裏可以判斷 ID 來決定要進行怎樣的預先處理 例如:傳遞何種資料 ```kotlin= import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } @IBAction func jumpToSecond(_ sender: Any) { performSegue(withIdentifier: "nextSegue", sender: sender) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { print(segue.identifier ?? "無") if segue.identifier == "nextSegue" { //TODO: 資料傳遞 } } } ``` ## Storyboard ID 跳轉 在 Storyboard 設定目的地的 Storyboard ID,並使用程式碼進行跳轉 由於它不像 Segue 有 prepare 的週期,所以管理跳轉流程時,需要特別設計、注意 優點是可以在程式碼中,設定跳轉的目的地,比起 Segue 動態,更改跳轉流程不用拉線 缺點是程式碼較多,且複雜些。 在下一頁面的 ViewController 設定 Custom Class 的 Class 為 SecViewController 設定 Identity 的 Storyboard ID 為 nextPage ![](https://hackmd.io/_uploads/HyC4jwCV3.png) 輸入程式碼,設定 Button 點擊事件,初始化名為 Main 的 storyBoard (即.storyboard檔案之名) 實體化 ID 為 nextPage 的 ViewController,並強制轉型成 SecViewController 進行 present 跳轉 *被註解的程式碼為使用 push 跳轉 欲跳轉的前一頁,需要在 Storyboard 中設定 Navigation Controller 才可使用 ```kotlin= import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } @IBAction func jumpToSecond(_ sender: Any) { let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) let vc = storyBoard.instantiateViewController(identifier: "nextPage") as! SecViewController present(vc, animated: true, completion: nil) //navigationController?.pushViewController(vc, animated: true) } } ``` ### 設定 Navigation Controller 放置 Navigation Controller 元件,把附帶的 Root View Controller 刪除 右鍵 Navigation Controller,將 root view controller 拖曳至 ViewController 完成後會產生標題欄,點擊可以設定 Title、Back Button 的文字,也可以新增Bar Button Item 如下圖所示,這樣就可以使用 Push, Push 出來的頁面會自帶標題欄、返回鍵 ![](https://hackmd.io/_uploads/BJMIsPCE3.png) ## 資料傳遞至新頁面 在 SecViewController 設定並公開 data 變數,讓 ViewController 傳遞資料 設定 dataLabel 顯示傳過來的 data **SecViewController** ```kotlin= import UIKit class SecViewController: UIViewController { @IBOutlet weak var dataLabel: UILabel! var data = "無資料傳入" override func viewDidLoad() { super.viewDidLoad() dataLabel.text = data } } ``` **ViewController** 使用 Segue 方式跳轉 ```kotlin= override func prepare(for segue: UIStoryboardSegue, sender: Any?) { print(segue.identifier ?? "無") if segue.identifier == "nextSegue" { let vc = segue.destination as! SecViewController vc.data = "使用 Segue 傳遞" } } ``` 使用 StoryBoard ID 方式跳轉 ```kotlin= @IBAction func jumpToSecond(_ sender: Any) { let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) let vc = storyBoard.instantiateViewController(identifier: "nextPage") as! SecViewController vc.data = "使用 StoryBoard ID 傳遞" present(vc, animated: true, completion: nil) //navigationController?.pushViewController(vc, animated: true) } ``` ## 資料返回原頁面 資料回傳(Callback)分為三種 - [Delegate(委派)](http://swift.diagon.me/delegate-weak/):將工作委派給其他頁面去實作,通過設定 Protocol (協議),將資料透過協議內的函式回傳,Protocol 如同 Java、Kotlin 語言中的 Interface (介面) - Closure(閉包):將程式打包成獨立區塊,實作的內容寫在區塊中,在返回前執行閉包並傳入參數,而前一頁收到資料後,執行獨立區塊中的內容,跟 Kotlin 語言中的 Closure 相同 - Notification(通知):如同 Android 的廣播,其原理就是觀察者模式,讓對象加入觀察者,並指定收到訊息的動作,而 NotificationCenter 會將訊息廣播給觀察者,藉此達到資訊傳遞 ==差異== - Delegate、Closure:只能一對一傳輸、可以雙向傳遞、傳遞效率高、需指定傳遞對象 - Notification:可進行多數對象傳輸、只能單向傳遞、傳遞效率低、不需指定傳遞對象 ==其他特性分析== | 特性 | Delegate | Closure | Notification | |:---:|:--------:|:-------:|:------------:| | 用途 | 可以針對不同的功能指定不同的函式執行 | 只能指定一個函式執行功能 | 只能指定一個函式執行功能 | | 難易度 | 使用設定較複雜,程式的連貫性差 | 較容易設定,程式的連貫性較佳 | 使用設定較複雜,程式的連貫性差 | | 安全性 | 安全,不容易記憶體遺失 | 不安全,使用不當容易記憶體洩漏 | 安全,不容易記憶體遺失 | | 偵錯 | 容易偵錯,若未新增「義務」函式會有錯誤提醒 | 不容易偵錯,錯誤需執行程式時才會發現 | 難以偵錯,錯誤需執行程式時才會發現 | | 使用時機 | Callback 函式大於一個、單一對象 | Callback 函式只有一個、單一對象 | Callback 函式只有一個、多數對象 | :::success 以下程式會將返回的資料,用於更改標題欄的名字 所以要先在 Storyboard 上設定 Navigation Controller(上面章節有教) ::: ### Delegate(委派) 在 SecViewController 訂定名為 SecViewControllerDelegate 的協議 宣告並公開 delegate 變數,讓外部進行委派,在需要的地方執行協議的函式,通知履行者 在 ViewController 拓展 SecViewControllerDelegate,實作協議要執行的內容 完成後,切換新頁面就會更改前一頁的標題名稱 **SecViewController** ```kotlin= import UIKit class SecViewController: UIViewController { //宣告 delegate 為 weak,避免循環引用(retain cycle) weak var delegate: SecViewControllerDelegate? override func viewDidLoad() { super.viewDidLoad() //通知有履行協議 changeTitle 的人,執行其實作的內容 delegate?.changeTitle(title: "執行過囉!") } } //protocol 是 Obective-C 才有的語法,在 Swift 中需要加上 @objc @objc protocol SecViewControllerDelegate { //必定要遵守並實作(如同:義務要履行) func changeTitle(title: String) //不一定要遵守與實作(如同:權利不必使用) @objc optional func others() } ``` **ViewController** 拓展並實作 SecViewControllerDelegate ```kotlin= /* 可用繼承、拓展方式實作協議,但繼承會將實作內容寫於 ViewController 的 class 中 若程式內容過多會不便管理,因此推薦使用拓展的方式,將實作內容寫於 class 外 */ extension ViewController: SecViewControllerDelegate { func changeTitle(title: String) { self.title = title } } ``` 使用 Segue 方式跳轉 ```kotlin= override func prepare(for segue: UIStoryboardSegue, sender: Any?) { print(segue.identifier ?? "無") if segue.identifier == "nextSegue" { let vc = segue.destination as! SecViewController vc.delegate = self //將 vc.delegate 委派給自己 } } ``` 使用 StoryBoard ID 方式跳轉 ```kotlin= @IBAction func jumpToSecond(_ sender: Any) { let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) let vc = storyBoard.instantiateViewController(identifier: "nextPage") as! SecViewController vc.delegate = self //將 vc.delegate 委派給自己 present(vc, animated: true, completion: nil) //navigationController?.pushViewController(vc, animated: true) } ``` ### Closure(閉包) 在 SecViewController 宣告並公開 closure 變數,讓 ViewController 引用 在需要的地方執行 closure,並將參數傳入,讓引用的地方可以執行內部的程式 在 ViewController 引用 closure,並實作當有參數傳入時的動作 完成後,切換新頁面就會更改前一頁的標題名稱 **SecViewController** ```kotlin= import UIKit class SecViewController: UIViewController { var closure: ((String) -> ())! override func viewDidLoad() { super.viewDidLoad() //通知有引用 closure 的人,執行其內部的程式 closure("執行過囉!") } } ``` **ViewController** 使用 Segue 方式跳轉 ```kotlin= override func prepare(for segue: UIStoryboardSegue, sender: Any?) { print(segue.identifier ?? "無") if segue.identifier == "nextSegue" { let vc = segue.destination as! SecViewController //引用 vc.closure,實作當參數傳入時的動作 vc.closure = {(title: String) in self.title = title } } } ``` 使用 StoryBoard ID 方式跳轉 ```kotlin= @IBAction func jumpToSecond(_ sender: Any) { let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) let vc = storyBoard.instantiateViewController(identifier: "nextPage") as! SecViewController //引用 vc.closure,實作當參數傳入時的動作 vc.closure = {(title: String) in self.title = title } present(vc, animated: true, completion: nil) //navigationController?.pushViewController(vc, animated: true) } ``` ### Notification(通知) 在 SecViewController 設定按鈕狀態與點擊事件 若按鈕狀態為選中,則開啟計時器任務,每秒發送廣播 在 ViewController 設定按鈕跳轉至 SecViewController 在頁面消失時,判斷廣播是否開始,若無則加入觀察者 若有則移除觀察者、計時器任務,並重新加入觀察者 **SecViewController** ```kotlin= import UIKit class SecViewController: UIViewController { @IBOutlet weak var noticeBtn: UIButton! var timer : Timer? var time = 0 override func viewDidLoad() { super.viewDidLoad() //設定按鈕不同狀態所顯示的文字 noticeBtn.setTitle("廣播已關閉", for: .normal) noticeBtn.setTitle("廣播已開啟", for: .selected) } @IBAction func clickNotification(_ btn: UIButton) { openCounter(open: !btn.isSelected) //按鈕為未選中,則開啟計數 btn.isSelected = !btn.isSelected //將按鈕的狀態反轉 } func openCounter(open: Bool) { if open { //將計數時間歸零 time = 0 //執行計數,每秒發送廣播 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (Timer) in self.time = self.time + 1 var data = [String: Any]() data["hour"] = Int(self.time/3600) data["min"] = Int(self.time/60) data["sec"] = Int(self.time%60) data["isOpen"] = self.noticeBtn.isSelected data["timer"] = self.timer //廣播者設定頻道、頻道內容(欲傳遞資料),發送廣播 let name = NSNotification.Name("channel") NotificationCenter.default.post(name: name, object: data) } } else { //停止廣播 timer?.invalidate() } } } ``` **ViewController** ```kotlin= import UIKit class ViewController: UIViewController { var openNotification: Bool? = false //一開始廣播還未開始播放 var observer : NSObjectProtocol? //儲存加入廣播的對象 var timer : Timer? //儲存計數器對象 override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) //如果已開始收聽廣播,則在離開收音機後(離開頁面),取消聽眾資格(取消觀察者),再回到收音機前成為聽眾(重新加入觀察者) if let open = openNotification, open { guard let observer = observer else { return } NotificationCenter.default.removeObserver(observer) timer?.invalidate() addObserver() } else { //尚未成為聽眾就加入 if observer == nil { addObserver() } } } func addObserver() { //設定想收聽的頻道名稱,並成為聽眾(觀察者),最後將物件指派給 observer (用於之後移除觀察者) let name = NSNotification.Name("channel") observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: OperationQueue.main) { (notification) in //將傳遞回來的資料轉型,並設定標題 let data = notification.object as! Dictionary<String, Any> let hour = data["hour"] as! Int let min = data["min"] as! Int let sec = data["sec"] as! Int self.openNotification = data["isOpen"] as? Bool self.timer = data["timer"] as? Timer self.title = String(format:"%02d:%02d:%02d", hour, min, sec) } } } ``` ## 補充說明 ### Notification 因為 Notification 比較特殊,所以特別拉出來說明 在 iOS 中,廣播、本地通知、遠程通知都叫做 Notification,所以常常讓人搞混 本文前面提到的資料回傳,就是廣播的一種,接下來要詳細說明廣播這黨事 ==概述== 廣播其實就是一種觀察者模式,將資訊廣播給已加入的觀察者 收到訊息的觀察者,就可以進行相應的動作 ==註冊成為觀察者== 有兩種註冊方式,一種是 Selector,另一種是 Closure || Selector | Closure | |:--:|:--:|:--:| |通知呼叫方法|Selector|Closure| |註冊對象|自己指定|自動指定為self| |移除對象|直接移除|需用 NSObjectProtocol 移除| ```kotlin= //observer:要通知的對象、selector:通知所呼叫的方法、name:通知名稱、object:接收特定物件的通知(設定nil,則所有物件的通知都收) NotificationCenter.default.addObserver(observer: Any, selector: Selector, name: NSNotification.Name?, object: Any?) //forName:通知名稱、object:接收特定物件的通知、queue:通知所呼叫的方法所在的執行緒(設定nil,則在註冊時的執行緒上同步進行)、using:通知所呼叫的方法 let observer : NSObjectProtocol = NotificationCenter.default.addObserver(forName: NSNotification.Name?, object: Any?, queue: OperationQueue?, using: (Notification) -> Void) ``` ==移除觀察者== 兩種註冊方式,各自對應的移除方式如下 <font color=Tomato>*第二種移除方式對兩種註冊方法都可用</font> ```kotlin= //observer:觀察者對象、name:通知名稱、object:不知道? NotificationCenter.default.removeObserver(observer: Any, name: NSNotification.Name?, object: Any?) //observer:註冊時回傳的物件對象 (如果使用 Selector 註冊,observer 填入 self,也可用來移除該對象的所有觀察者) NotificationCenter.default.removeObserver(observer: Any) ``` :::info 從 iOS 9(和 OS X 10.11)開始,如果沒有使用閉包的觀察者,則不需要自己刪除觀察者 系統將為你完成,因為它可以為觀察者使用歸零弱引用。 如果你正在使用閉包的觀察者,請確保你捕捉弱引用[weak self]在封閉的捕獲列表 並在 deinit 刪除觀察者。 如果你不對self的弱引用做deinit,那麼觀察者將永遠不會移除 因為 Notification Center 將無限期地保留對它的強引用。 但最好的方式還是在離開頁面時,自己移除觀察者,最為安全。 ::: ## 補充資料&參考文章 [Prepare 週期](https://medium.com/彼得潘的-swift-ios-app-開發教室/簡易說明xcode中的資料傳遞-使用prepare傳遞property的方式-a5666ca36114) [Delegate(委派)](http://swift.diagon.me/delegate-weak/) [Retain cycle](https://ithelp.ithome.com.tw/articles/10196788) [Notification in Swift](https://medium.com/@dmytro.anokhin/notification-in-swift-d47f641282fa) [Swift 中刪除觀察者](https://stackoverflow.com/questions/28689989/where-to-remove-observer-for-nsnotification-in-swift/28690024#28690024) ## 懶人包重點 - 跳轉主要有兩種呈現方式:Push (頁面有先後順序)、Present (與上一頁面無關聯) - 執行跳轉的方式有兩種:Segue (有、無程式碼)、Storyboard ID (有程式碼) - Xib檔 只能用 Storyboard ID 跳轉、Storyboard檔 皆可用 - 資料傳遞至其他頁面時,Segue 有 Prepare 週期 - 資料返回上一頁面有三種方式:Delegate(委派)、Closure(閉包)、Notification(通知) ## 未完成內容 - 補充說明 seletor 的 removeObserver object 的用途待查 - 將程式碼改成 Swift 格式 (因 HackMD 沒有支援 Swift 語言,暫時用 Kotlin 代替)