iOS 開發 #15 用全編程設計的 UIKit 元件來做 App ( 2 )
介紹一下 UINavigationController
跟 UITabBarController
前言
在上篇講了如何用編程的方式來繪製 UI 與實現 Auto Layout
,而一個 App 的重點除了 UI 以外,還有頁面的切換也很重要,那這篇就來講講在 iOS 中兩個頁面切換的元件 UITabBarController
跟 UINavigationController
UITabBarController
先來說說 UITabBarController
,在上篇有說到如果不用 Storyboard
的方式來顯示 UI,需要到 SceneDelegate
裡面去設定 rootViewController
,應該還有印象吧……嗎
那如果要使用 UITabBarController
來實現頁面切換的話,需要到 SceneDelegate
把 rootViewController
設定成 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
後到 SceneDelegate
的 scene 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
來實現頁面切換的話,需要到 SceneDelegate
把 rootViewController
設定成 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
這邊放你要導向的 ViewController
,animated
則是是否有動畫,而在 popViewController
中因為這個是返回上一頁,所以他只有 animated
的參數來控制返回時是否有動畫
那用 UINavigationController
會有兩種情況
- 不傳值的頁面切換
- 傳值的頁面切換
先說不傳值的頁面切換
範例會用這兩個 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 的方式來實現頁面切換