---
# System prepended metadata

title: 頁面跳轉的大小事
tags: [Swift, iOS]

---

###### 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 代替)