iOS 開發 #15 用全編程設計的 UIKit 元件來做 App ( 2 )

介紹一下 UINavigationControllerUITabBarController

--

前言

在上篇講了如何用編程的方式來繪製 UI 與實現 Auto Layout,而一個 App 的重點除了 UI 以外,還有頁面的切換也很重要,那這篇就來講講在 iOS 中兩個頁面切換的元件 UITabBarControllerUINavigationController

UITabBarController

先來說說 UITabBarController,在上篇有說到如果不用 Storyboard 的方式來顯示 UI,需要到 SceneDelegate 裡面去設定 rootViewController,應該還有印象吧……嗎

那如果要使用 UITabBarController 來實現頁面切換的話,需要到 SceneDelegaterootViewController 設定成 UITabBarController

首先先建立兩個 ViewController

FirstViewController

import UIKit

class FirstViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let firstLabel = UILabel()
firstLabel.text = "First"
firstLabel.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(firstLabel)

NSLayoutConstraint.activate([
firstLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
firstLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}

#Preview {
UINavigationController(rootViewController: FirstViewController())
}

SecondViewController

import UIKit

class SecondViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let secondLabel = UILabel()
secondLabel.text = "Second"
secondLabel.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(secondLabel)

NSLayoutConstraint.activate([
secondLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
secondLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}

#Preview {
UINavigationController(rootViewController: SecondViewController())
}

建立完兩個 ViewController 後到 SceneDelegatescene function 中做設定

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }

// 宣告第一個 ViewController (FirstViewController)
let firstViewController = FirstViewController()

// 設定 firstViewController 的 tabBarItem
firstViewController.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "house"), tag: 0)

// 宣告第二個 ViewController (SecondViewController)
let secondViewController = SecondViewController()

// 設定 secondViewController 的 tabBarItem
secondViewController.tabBarItem = UITabBarItem(title: "Second", image: UIImage(systemName: "list.bullet"), tag: 1)

// 宣告一個 UITabBarController (tabBarController)
let tabBarController = UITabBarController()

// 把剛剛宣告的兩個 ViewController 加入到 tabBarController
tabBarController.viewControllers = [firstViewController, secondViewController]

window = UIWindow(windowScene: scene as! UIWindowScene)

// 設定 tabBarController 選擇第一頁
tabBarController.selectedIndex = 0

// 把剛剛設定完的 tabBarController 設定為 rootViewController
// 這樣執行 APP 時就會顯示剛剛選擇的頁面
window?.rootViewController = tabBarController
window?.makeKeyAndVisible()
}

設定完 SceneDelegate 後那來執行看看效果吧

講完 UITabBarController 後,接著講講 UINavigationController

UINavigationController

若要透過 UINavigationController 來實現頁面切換的話,需要到 SceneDelegaterootViewController 設定成 UINavigationController,先來講解 SceneDelegate 中的 scene function

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }

// 宣告一個 UINavigationController (navViewController)
// 並設定好 UINavigation 的 rootViewController
let navViewController = UINavigationController(rootViewController: FirstViewController())

window = UIWindow(windowScene: scene as! UIWindowScene)

// 接著將剛剛宣告的 navViewController 當作這個 App 的 rootViewController
window?.rootViewController = navViewController
window?.makeKeyAndVisible()
}

UINavigationController 是透過兩個參數來控制頁面切換

  • pushViewController(_ viewController:animated:)
  • popViewController(animated:)

pushViewController_ viewController 這邊放你要導向的 ViewControlleranimated 則是是否有動畫,而在 popViewController 中因為這個是返回上一頁,所以他只有 animated 的參數來控制返回時是否有動畫

那用 UINavigationController 會有兩種情況

  1. 不傳值的頁面切換
  2. 傳值的頁面切換

先說不傳值的頁面切換

範例會用這兩個 ViewController 來做示範

NavFirstViewController :

import UIKit

class NavFirstViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let btnFirst = UIButton()
btnFirst.translatesAutoresizingMaskIntoConstraints = false
btnFirst.setTitle("Next", for: .normal)
btnFirst.setTitleColor(.black, for: .normal)
btnFirst.backgroundColor = .opaqueSeparator
btnFirst.addTarget(self, action: #selector(nextPage), for: .touchUpInside)

view.addSubview(btnFirst)

NSLayoutConstraint.activate([
btnFirst.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnFirst.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

@objc private func nextPage() {}
}

#Preview {
UINavigationController(rootViewController: NavFirstViewController())
}

NavSecondViewController :

import UIKit

class NavSecondViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let btnSecond = UIButton()
btnSecond.translatesAutoresizingMaskIntoConstraints = false
btnSecond.setTitle("Back", for: .normal)
btnSecond.setTitleColor(.black, for: .normal)
btnSecond.backgroundColor = .opaqueSeparator
btnSecond.addTarget(self, action: #selector(backPage), for: .touchUpInside)

view.addSubview(btnSecond)

NSLayoutConstraint.activate([
btnSecond.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnSecond.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

@objc private func backPage() {}

}

#Preview {
UINavigationController(rootViewController: NavSecondViewController())
}

不傳值的頁面切換

若要從 NavFirstViewController 切換到 NavSecondViewController 的話只需要使用剛剛提到的 pushViewController() ,而要從 NavSecondViewController 返回到 NavFirstViewController 的話則需要使用剛剛提到的 popViewController()

NavFirstViewController:

import UIKit

class NavFirstViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let btnFirst = UIButton()
btnFirst.translatesAutoresizingMaskIntoConstraints = false
btnFirst.setTitle("Next", for: .normal)
btnFirst.setTitleColor(.black, for: .normal)
btnFirst.backgroundColor = .opaqueSeparator
btnFirst.addTarget(self, action: #selector(nextPage), for: .touchUpInside)

view.addSubview(btnFirst)

NSLayoutConstraint.activate([
btnFirst.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnFirst.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

@objc private func nextPage() {

// 在這邊使用 pushViewController 來導向到 NavSecondViewController
navigationController?.pushViewController(NavSecondViewController(), animated: true)
}
}

#Preview {
UINavigationController(rootViewController: NavFirstViewController())
}

NavSecondViewController:

import UIKit

class NavSecondViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let btnSecond = UIButton()
btnSecond.translatesAutoresizingMaskIntoConstraints = false
btnSecond.setTitle("Back", for: .normal)
btnSecond.setTitleColor(.black, for: .normal)
btnSecond.backgroundColor = .opaqueSeparator
btnSecond.addTarget(self, action: #selector(backPage), for: .touchUpInside)

view.addSubview(btnSecond)

NSLayoutConstraint.activate([
btnSecond.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnSecond.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

@objc private func backPage() {

// 在這邊使用 popViewController 來回到上一頁
navigationController?.popViewController(animated: true)
}

}

#Preview {
UINavigationController(rootViewController: NavSecondViewController())
}

傳值的頁面切換

傳值的頁面切換的話需要先在目標 ViewController 中建立一個變數後,在 @objc func 中將資料傳過去

這邊先貼 NavSecondViewController

NavSecondViewController :

import UIKit

class NavSecondViewController: UIViewController {

// 用來接收資料的
var receivedText: String?

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let btnSecond = UIButton()
btnSecond.translatesAutoresizingMaskIntoConstraints = false

// 將接收到的資料顯示在 button title 裡面
// 但因為 receivedText 是 Optional
// 為了避免因 receivedText 為 nil 而發生閃退
// 所以在 receivedText 後面加上 ?? "Back"
// 確保當他真的為 nil 時也有東西顯示
btnSecond.setTitle(receivedText ?? "Back", for: .normal)
btnSecond.setTitleColor(.black, for: .normal)
btnSecond.backgroundColor = .opaqueSeparator
btnSecond.addTarget(self, action: #selector(backPage), for: .touchUpInside)

view.addSubview(btnSecond)

NSLayoutConstraint.activate([
btnSecond.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnSecond.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

@objc private func backPage() {
navigationController?.popViewController(animated: true)
}

}

#Preview {
UINavigationController(rootViewController: NavSecondViewController())
}

NavFirstViewController :

import UIKit

class NavFirstViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
setupView()
}

private func setupView() {
view.backgroundColor = .white

let btnFirst = UIButton()
btnFirst.translatesAutoresizingMaskIntoConstraints = false
btnFirst.setTitle("Next", for: .normal)
btnFirst.setTitleColor(.black, for: .normal)
btnFirst.backgroundColor = .opaqueSeparator
btnFirst.addTarget(self, action: #selector(nextPage), for: .touchUpInside)

view.addSubview(btnFirst)

NSLayoutConstraint.activate([
btnFirst.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnFirst.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

@objc private func nextPage() {
let navSecVC = NavSecondViewController()

// 這邊把 Hello World! 傳到 NavSecondViewController 的 receivedText
navSecVC.receivedText = "Hello, World!"
navigationController?.pushViewController(navSecVC, animated: true)
}
}

#Preview {
UINavigationController(rootViewController: NavFirstViewController())
}

這是這次示範 App 的 git 連結,有興趣可以 Clone 下來玩玩看

後記

在上篇簡單介紹要怎麼生出 UIKit 跟如何用 Code 的方式來實現 Auto Layout,而在這篇則在講如何用 Code 的方式來實現頁面切換

--

--