Controller之間的呼叫

present 與 dismiss

想要跳轉頁面,我們可以靠storyBoard拉出present的線,也可以靠程式碼。
在StoryBoard中只要按住ctrl,將button拉到另一個controller後再選present即可。
在程式碼的話

1
2
// 因為此ViewController什麼都沒設定,所以會是一片黑
present(ViewController(), animated: true, completion: nil)

有跳過去,就有返回,想返回時使用dismiss function。

1
dismiss(animated: true, completion: nil)

dismiss的原理是,直接呼叫時,請求presentingViewController把你關掉。
間接呼叫時,把後面的presentedViewController們關掉。
至於這兩個是什麼意思請參看下節。

presentingViewController 與 presentedViewController

每個controller皆有兩個重要的屬性,可以分辨這個controller是被誰present,或是present了誰。

  • presentingViewController:present Controller的Controller
  • presentedViewController:被present的Controller

假設ABC皆為Controller,A present B,B present C,那對於B來說。

  • A為B之presentingViewController
  • C為B之presentedViewController

Navigation bar

push 與 pop

與present / dismiss相似,進到navigation controller後要使用 navigation屬性的push/pop方法,不然會跳出navigation controller的頁,失去Navigation Bar的效果。

程式碼的形式如下

1
2
3
4
5
6
// 推上下一個頁面
self.navigationController?.pushViewController(ViewController(), animated: true)
// 回到上一頁
self.navigationController?.popViewController(animated: true)
// 直接回到navigation最開頭
self.navigationController?.popToRootViewController(animated: true)

前後的controller

如同present / dismiss可以拿到前後關係一樣,navigation controller也是可以拿到前後關係的。
所有經過的controller都放在self.navigationController?.viewControllers這個array裡可以拿取。

1
2
3
self.navigationController?.viewControllers[0]
self.navigationController?.viewControllers[1]
...

Navigation Contrller本身並不帶有頁面資訊,而是在Navigation Controller的下一個Controller才會開始顯示。
那兩者之間要建立連線,必須要指定Navigation Controller當中的Root View Controller才行。

  • 方法1: Navigation綁定Root Controller,有兩種做法
    • 按住Ctrl並將Navigation Controller拉到想要顯示的第一個Controller,鬆開後選擇Root view Controller
    • 在Navigation Controller的Triggered Segue中,指定Root view Controller
  • 方法2: 在StoryBoard中,點擊已經存在的Controller。並從上方選單Editor => Embed in => NavigatioController即可

上面的title和向右鈕

預設只有Navigation Controller後第一個Bar可以編輯Title,若要加入新title則要從StoryBoard中拉入Navigation item
預設上方並無向右鍵,只有返回鍵,若要加入則拉入Bar Button Item

Tab Bar Controller

與Navigation View許多做法類似,想加上新的tab可以

  • 方法1: TabViewController綁定View Controllers,有兩種做法
    • 按住Ctrl並將TabViewController拉到想要加上的Controller,鬆開後選擇View Controllers
    • 在TabViewController的Triggered Segue中,指定新的View Controllers
  • 方法2: 在StoryBoard中,點擊已經存在的Controller。並從上方選單Editor => Embed in => TabViewController即可

tab bar controller下的controller們

1
2
3
self.tabBarController?.viewControllers[0]
self.tabBarController?.viewControllers[1]
...

要注意的是,如果是tab bar controller下包著navigation controller,在存取時要先轉型成navigation controller,再取用navigation controller裡面的controller,才不會錯誤。

切換tab

1
self.tabBarController?.selectedIndex = 0

隱藏tab

1
self.tabBarController?.tabBar.isHidden = true

在不同的Controller之間跳轉

方法1 直接拖拉

在storyboard上
按住ctrl點下來源Controller之button拉到要呈現的Controller後鬆開,選擇show或present即可。
show會在啟用navigation bar的時候自動使用navigation.push,若無navigation bar則是使用present。

方法2 利用segue

在storyboard上
按住ctrl點下來源Controller,拉到要呈現的Controller,會多一條segue連線,幫此條連線取名字(identifer)
接下來在想切換的情況下使用performSegue即可(參數為剛剛取的identifer)

1
performSegue(withIdentifier: "goToView2", sender: nil)

方法3 指定StoryBoard以及Controller (通常用在更換Story Board的時候)

將要呈現的Controller取名字(StoryBoardID)
並在程式碼中指定

  1. 位於哪個StoryBoard
  2. Controller的StoryBoardID

最後present或push即可

1
2
3
let myStoryBoard = UIStoryboard(name: "Main", bundle: nil)
let whiteViewController = myStoryBoard.instantiateViewController(withIdentifier: "whiteView")
present(whiteViewController, animated:true, completion:nil)

如果是位於同一個storyboard,也可以拿到自己的storyboard

1
2
3
let myStoryBoard = self.storyboard!
let whiteViewController = myStoryBoard.instantiateInitialViewController(withIdentifier: "whiteView")
present(whiteViewController, animated:true, completion:nil)

方法4 StoryBoard Reference (通常用在更換Story Board的時候)

在StoryBoard中,將StoryBoard Reference
拖到畫面上,指定StoryBoard Reference上的StoryBoard和ControllerID
接下來利用方法1或2拖拉即可成功

方法5 逃生門 (通常用在要返回前一個Controller的時候)

情況

假設最初的controller取名叫parent controller
parent controller可以連到許多child controller
當child controller想返回parent controller的時候

作法

先寫在parent controller

1
2
3
@IBAction func backToMain(_ segue:UIStoryboardSegue){
	print("back to main")
}

再用StoryBoard選擇child controller
按鈕按住ctrl往該controller的逃生門拉 (上方最右邊的圖示)
即會出現backToMain的方法
如果parent controller在child controller返回後想執行什麼動作也可在此function中填入

Controller之間傳遞訊息

方法1. 利用destination controller的property (呼叫下一個Controller時)

  1. 複寫Source Controller的prepare Function
  2. 參數segue之屬性destionation即為destination controller
  3. 強制轉型為destination controller
  4. 將其塞入數值

這裡要注意的是,不能把值塞進destination controller的UI元件內(Ex: Label),因為這個時候畫面元件還沒生成。

1
2
3
4
5
6
7
// Source Controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
	if segue.destination is SecondViewController{
    let des = segue.destination as! SecondViewController
  	des.textFromFirstView = "Hello World"
	}
}

方法2. 利用前後階層關係(僅適用於返回controller時)

前面有說過present可以靠

  • presentingViewController
  • presentedViewController

navigation bar可以靠

  • self.navigationController?.viewControllers[0]
  • self.navigationController?.viewControllers[1]

來獲得前面的controller元件 取得controller後再參考方法1塞入值

方法3. protocol (返回前一個controller時)

將前一個controller名為FirstViewController
現在這個controller名為SecondViewController
令一個protocol叫做SecondViewControllerDelegate

  1. 將SecondViewControllerDelegate當作SecondViewController的屬性
  2. 在FirstViewController跳轉到SecondViewController前,通過方法1塞進自己實作的SecondViewControllerDelegate
  3. SecondViewController消失前,執行該protocol的method (透過複寫viewWillDisapear來達成)

SecondController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var delegate:SecondViewControllerDelegate?
@IBAction func goBack(_ sender: UIButton) {
	let _ = navigationController?.popViewController(animated: true)
}

override func viewWillDisappear(_ animated: Bool) {
  super.viewWillDisappear(animated)
  if self.isMovingFromParentViewController{
		delegate?.setColor(colorType: "red")
	}
}

hint: 可以靠 self.isMovingFromParentViewController來測試是否為返回到前一個Controller

SecondViewControllerDelegate

1
2
3
protocol SecondViewControllerDelegate{
	func setColor(colorType:String)
}

FirstViewController (實作SecondViewControllerDelegate)

1
2
3
4
5
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if let des = segue.destination as? SecondViewController{
    des.delegate = self
  }
}

方法4. notification (返回前一個controller時)

  1. parent controller先監聽事件
    • 撰寫接收到監聽後要執行的function
    • 註冊監聽事件
  2. child controller發送notification

ParentController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 接收到事件後要執行的任務
func getUpdateNoti(noti:Notification) {
	let data = noti.userInfo!["data"] as! String
	print(data)
}

override func viewDidLoad() {
  super.viewDidLoad()
	
  // 註冊事件
  let notificationName = Notification.Name("GetUpdateNoti")
  NotificationCenter.default.addObserver(self, selector: #selector(ViewController.getUpdateNoti(noti:)), name: notificationName, object: nil)
}

ChildController

1
2
3
// 發送事件
let notificationName = Notification.Name("GetUpdateNoti")
NotificationCenter.default().post(name: notificationName,object: nil, userInfo: ["data":"Hello World"])

方法5. 全域變數

在AppDelegate增加property
並在想要使用的class裡面,將UIApplication.shared.delegate轉型為AppDelegate
即可使用裡面的property

1
2
3
4
5
if let appDelegate = UIApplication.shared.delegate as? AppDelegate{
	if let colorName = appDelegate.color{
    setColor(colorType: colorName)
  }
}

全域變數還有很多做法,像是singleton或是寫在class之外,有機會再開一篇講。