###### 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 拖曳至下一畫面,並選擇呈現方式

完成後會在兩個 ViewController 之間產生一條連接線,這樣就完成跳轉囉!

### Segue - 有程式碼
點 ViewController (上方三個圖示最左邊) 按右鍵,將 manual 拖曳至下一畫面,並選呈現方式

兩個 ViewController 間產生一條連接線,點擊連接線,可以看到它對應到整個 ViewController

點擊連接線,設定 Storyboard Segue Identifier 為 nextSegue

輸入程式碼,設定 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

輸入程式碼,設定 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 出來的頁面會自帶標題欄、返回鍵

## 資料傳遞至新頁面
在 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 代替)