UINavigationController의 동작 방식
내비게이션 컨트롤러란 계층으로 구성된 스택 기반의 뷰 컨트롤러입니다.
아이폰 유저라면 이미 모두 한 번쯤은 이미 사용해 봤을 거예요.
그림을 보며 동작방식을 이해를 해봅시다.
처음 보이는 [Setting 컨트롤러]는 Root View 컨트롤러입니다.
[Setting 컨트롤러]에서 General을 선택하면, 대리자를 통하여 재정의한 push
동작에 의해
새로운 [General 뷰 컨트롤러]를 생성하고 [Setting 뷰 컨트롤러] 위에다 쌓는 형식이죠.
스택을 어떻게 쌓는지 간단히 알아봅시다. (push or show)
내비게이션 스택은 순서가 필요하니 배열을 사용합니다.
배열 viewControllers
내부에 뷰 컨트롤러들을 저장하죠.
내비게이션 스택 == viewControllers
현재 위치는 [Setting 뷰 컨트롤러]라고 생각해 봅시다.
다음 화면으로 넘어가기 위해 대리자를 통하여 push
를 하게 되면
첫 번째로 [Setting 뷰 컨트롤러] 다음으로 [General 뷰 컨트롤러], 마지막으로 [Auto-Lock 뷰 컨트롤러]
이와 같은 형식으로 배열에 순차적으로 쌓이게 됩니다.
NavigationVC = [SettingVC, GeneralVC, Auto-LockVC]
스택 형식으로 쌓여 있기 때문에
다음 뷰 컨트롤러로 push
이 될 때 기존의 뷰 컨트롤러는 메모리에서 제거되지 않아요!
메모리가 제거가 되지 않기 때문에 사용 시 메모리 관리도 생각해줘야 할 거예요.
(스택이 많이 쌓으면 메모리 낭비가...)
반대로 제거를 할 때 (pop)
뒤로 가기 버튼 또는 왼쪽 가장자리 스와이프 통해
이전 뷰 컨트롤러로 넘어갈 수 있습니다.
또는 popToRootViewController
메서드를 통해 기존에 생성된 모든 내비게이션 스택을 지우고
한 번에 루트 뷰로 이동 시키는 메서드도 존재합니다.
메모리는 pop
될 때 뷰 컨트롤러 인스턴스가 다른 곳에서 참조되지 않으면
메모리에서 해제되고, 내비게이션 스택에서 삭제됩니다.
구조
내비게이션 컨트롤러의 내부 구조는
그림과 같이 4가지로 나뉩니다.
viewControllers, navigationBar, toolbar, delegate
viewControllers
"s" 복수형이 붙어 있네요.
즉, viewControllers는 여러 개의 ViewController
를 관리하는 컨트롤러라는 것을 알 수 있습니다.
내비게이션 컨트롤러의 객체에서 여러 뷰 컨트롤러들을 관리하기 위해서
일명 내비게이션 스택이라는 것을 통해 관리를 합니다.
내비게이션 스택 == viewControllers
내비게이션 스택은 배열을 통하여 순서대로 뷰 컨트롤러들을 관리하죠.
내비게이션 스택은 ViewController
들은 배열 형태로 갖고 있습니다.
push
또는 pop
으로 생성 및 제거 관리를 할 수 있습니다.
또한 viewControllers의 인덱스 접근을 통해 뷰 컨트롤러의 객체에도 접근이 가능하죠.
밑에 예제를 참고하세요.
navigationBar
앱의 상단의 특정 영역에 생성되는 UI 요소입니다.
기본 구성 요소로는 뒤로 가기 버튼, 가운데 제목, 선택적으로 생성하는 오른쪽 버튼이 있습니다.
내비게이션 컨트롤러에서 push
를 할 때, push
한 뷰 컨트롤러에서는 따로 설정을 안 해줘도
자동적으로 내비게이션 바가 생성이 됩니다.
만약 다른 뷰 컨트롤러의 내비게이션 바의 UI 변경을 원한다면
내비게이션 바의 재정의를 통하여 UI 설정도 가능합니다.
자세한 내용은 [iOS/Swift] UINavigationBar 사용 방법 을 참고 해주세요.
toolBar
인터페이스의 아래쪽에 하나 이상의 버튼을 표시하는 컨트롤입니다.
기본적으로 자동 생성되지 않고
내비게이션 컨트롤러의 속성 isToolbarHidden
은 기본값이 true
이므로
false
로 변경하면 생성이 됩니다.
그리고 toolbarItems에 원하는 버튼을 추가하여 사용하면 됩니다.
자세한 내용은 [iOS/Swift] NavigationController의 ToolBar를 코드로 만들어 봅시다 를 참고 해주세요.
delegate
Apple이 개발의 편의를 위하여 모든 Contorller
들은 delegate
가 선언이 돼있습니다.
특정 Event 관련 처리를 위해서 대리자를 미리 만들어 놓은 거죠.
내비게이션 컨트롤러에서는 push
, pop
될 때의 Event를 처리합니다.
보통 특정 뷰 컨트롤러의 생성 관련 Event 및 화면 이동 간 애니메이션 설정을 위해 사용합니다.
NavigationController 사용 방법
스토리보드는 생략하고 바로 코드로 작성하는 법을 알아봅시다.
내비게이션 컨트롤러는 앱 실행 시 바로 사용되므로
SceneDelegate
에서 코드를 작성해 줍시다.
이렇게 뚝딱 내비게이션 컨트롤러가 만들어졌습니다.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let mainViewController = ViewController()
// NavigationController를 생성해주고 루트 뷰를 설정합니다.
let navigationVC = UINavigationController(rootViewController: mainViewController)
// window에 보여질 루트 뷰를 NavigationController로 설정
self.window?.rootViewController = navigationVC
// makeKeyAndVisible
// window를 표시하고 Key window로 설정(window를 앞으로 배치)
// key window: window가 여러개 존재할 때, 가장 앞쪽에 배치된 window를 `key window`라고 지칭합니다.
self.window?.makeKeyAndVisible()
self.window = window
}
다른 컨트롤러로 이동하는 방법으로는
push
, show
이렇게 두 가지 방법이 있습니다.
// push
self.navigationController?.pushViewController(SecondViewController(), animated: true)
// show
self.show(SecondViewController(), sender: nil)
한번 사용해 볼까요?
간단히 버튼을 만들어 주고 push
로 SecondViewController
로 이동해 보겠습니다.
class ViewController: UIViewController {
lazy var button: UIButton = {
let btn = UIButton(frame: CGRect(x: view.frame.width/2 - 75, y: 400, width: 150, height: 50),
primaryAction: UIAction(handler: { _ in
// 버튼 동작으로 push하기
self.navigationController?.pushViewController(SecondViewController(), animated: true)
}))
btn.setTitle("NextVC", for: .normal)
btn.backgroundColor = .systemBlue
return btn
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .gray
self.view.addSubview(button)
// 네이게이션바 타이틀 설정
self.navigationItem.title = "FirstVC"
}
}
내비게이션을 통한 push
를 하게 되면
아래와 같이 자동으로 내비게이션 바가 생성되는 것을 볼 수 있네요. (뒤로 가기 버튼)
완성
조금 더 응용을 해봅시다.
내비게이션 스택은 push
를 하면 순차적으로 배열에 추가되고
뒤로 가기(pop
)를 하게 되면 배열에서 순차적으로 제거되는 형식으로 작동합니다.
배열에 추가된다고 했는데 어디에 저장이 될까요?
바로 viewControllers
에서 배열로 저장이 됩니다.
let navigationStack: [UIViewController] = navigationController?.viewControllers
이걸 조금 응용하면 데이터도 전달할 수 있죠.
index 0부터 배열에 순차적으로 쌓이므로
이와 같이 접근도 가능합니다.
let fisrtIndex = navigationController!.viewControllers.startIndex
let firstVC = navigationController?.viewControllers[fisrtIndex] as! FirstViewController
FirstViewController
에서
데이터를 받을 UILabel을 생성해 줬습니다.
class FirstViewController: UIViewController {
lazy var label: UILabel = {
let lable = UILabel(frame: CGRect(x: view.frame.width/2 - 125, y: 300, width: 250, height: 50))
lable.font = .systemFont(ofSize: 20, weight: .bold)
return lable
}()
lazy var button: UIButton = {
let btn = UIButton(frame: CGRect(x: view.frame.width/2 - 75, y: 400, width: 150, height: 50),
primaryAction: UIAction(handler: { _ in
self.navigationController?.pushViewController(SecondViewController(), animated: true)
}))
btn.setTitle("NextVC", for: .normal)
btn.backgroundColor = .systemBlue
return btn
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .gray
self.view.addSubview(label)
self.view.addSubview(button)
// 네이게이션바 타이틀 설정
self.navigationItem.title = "FirstVC"
}
}
SecondViewController
에서 배열 viewControllers
를 통하여
FirstViewController
에 접근해서 FirstViewController
의 UILabel
을 업데이트시켰어요.
class SecondViewController: UIViewController {
lazy var button: UIButton = {
let btn = UIButton(frame: CGRect(x: view.frame.width/2 - 75, y: 400, width: 150, height: 50),
primaryAction: UIAction(handler: { _ in
// 서브스크립트로 접근하기
let fisrtIndex = self.navigationController!.viewControllers.startIndex
let firstVC = self.navigationController?.viewControllers[fisrtIndex] as! FirstViewController
// 데이터 전달
firstVC.label.text = "여기는 SecondVC이다. 오바!"
self.navigationController?.popViewController(animated: true)
}))
btn.setTitle("전달하고 뒤로가기", for: .normal)
btn.backgroundColor = .systemBlue
return btn
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
view.addSubview(button)
self.navigationItem.title = "SecondVC"
}
}
스토리보드를 사용하지 않고 작성하다 보니 코드가 정신없네요.
중요한 키워드는 viewControllers
의 배열을 통해 뷰 컨트롤러로 접근할 수 있다는 것만
기억하시고 계시면 될 것 같네요
코드로 TabBar 생성 방법
TabBar는 앱의 하단에 생성이 되며 선택한 아이템에 따라
뷰 컨트롤러를 선택할 수 있는 다중 인터페이스입니다.
⭐️ 여기서 헷갈릴 수 있는데 ⭐️
TabBar와 ToolBar와는 비슷하지만 모양 서로 완전히 다른 클래스이고 목적이 다릅니다!
UINavigationController에 속한 게 아닌 UIView에 속해있습니다.
(TabBar - UIView), (ToolBar - UINavigationController)
원하는 상황에 맞게 사용해야 합니다.
그럼 왜 여기서 알려주나요?
이유는 UINavigationController와 마찬가지로
TabBar는 보통 처음 앱이 실행되는 시점에서 만들어 사용되기 때문입니다.
그래서 SceneDelegate에서
UINavigationController와 같이 TabBar를 사용하는 예제를 준비해 봤습니다.
그럼 이제
간단하게 기본 앱 시계의 탭바를 만들어 보겠습니다.
일단 아래와 같이 컨트롤러를 만들어줄게요.
내비게이션 컨트롤러를 생성하는 방법은 위와 동일하고
탭바 컨트롤러를 생성 및 설정을 해주기만 하면 됩니다.
설명이 많으므로 한 번에 주석으로 설명하겠습니다.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
// 탭바 컨트롤러를 생성합니다.
let tabBarVC = UITabBarController()
// 시계 앱을 켜보면 worldTimeVC와 alarmVC가 각각 네비게이션 바를 갖고 있으므로
// 각각의 컨트롤러를 네비게이션 컨트롤러로 설정을 해줬습니다.
// 밑에 추가하기 작업을 위해 미리 다른 컨트롤러들의 인스턴스 생성함.
let worldTimeVC = UINavigationController(rootViewController: WorldTimeViewController())
let alarmVC = UINavigationController(rootViewController: AlarmViewController())
let stopWatchVC = StopWatchViewController()
let timerVC = TimerViewController()
// 뷰 컨트롤러들을 탭바로 사용하기 위해 배열에 추가를 해줍시다.
// 배열의 Index 0 부터 순서대로 뷰 컨트롤러를 위치 합니다.
tabBarVC.setViewControllers([worldTimeVC, alarmVC, stopWatchVC, timerVC], animated: false)
// 기타 설정들
tabBarVC.modalPresentationStyle = .fullScreen
tabBarVC.tabBar.backgroundColor = .black
tabBarVC.tabBar.tintColor = .orange
tabBarVC.tabBar.unselectedItemTintColor = .gray
// 탭바 이름을 설정해 줍니다.
worldTimeVC.title = "세계 시계"
alarmVC.title = "알람"
stopWatchVC.title = "스톱워치"
timerVC.title = "타이머"
// 탭바 이미지 설정 (UIImage를 통해)
guard let items = tabBarVC.tabBar.items else { return }
items[0].image = UIImage(systemName: "globe")
items[1].image = UIImage(systemName: "alarm.fill")
items[2].image = UIImage(systemName: "stopwatch.fill")
items[3].image = UIImage(systemName: "timer")
// 기본루트뷰를 탭바 컨트롤러로 설정
window.rootViewController = tabBarVC
window.makeKeyAndVisible()
self.window = window
}
완성
이렇게 오늘인 내비게이션 컨트롤러 및 탭바 사용 방법에 대하여 알아봤습니다.
부족한 설명이지만, 조금은 이해 가셨나요?
틀린 내용이 있다면 언제든지 지적해 주시면 감사히 받겠습니다. 🫠
읽어주셔서 감사합니다 😃
다음번에는 내비게이션 바 설정 방법에 대하여 다뤄보도록 하겠습니다.
참고
'UIKit > Swift' 카테고리의 다른 글
[iOS/Swift] 디버깅을 통해 메모리 누수를 찾아보자(Debug, Memory Leaks) (0) | 2023.02.17 |
---|---|
[iOS/Swift] MVVM 패턴의 Data Binding에 대해서 알아보자! (Closure, Observable, Combine) (0) | 2023.02.14 |
[iOS/Swift] UINavigationBar 사용 방법 (0) | 2023.02.07 |
[iOS/Swift] 델리게이트 패턴 (Delegate) (0) | 2022.12.23 |
[iOS/Swift] UITextField 설정 (0) | 2022.12.20 |