Danny의 iOS 컨닝페이퍼
article thumbnail

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)

한번 사용해 볼까요?

 

간단히 버튼을 만들어 주고 pushSecondViewController로 이동해 보겠습니다.

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에 접근해서 FirstViewControllerUILabel을 업데이트시켰어요.

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
}

 

완성

 

 


이렇게 오늘인 내비게이션 컨트롤러 및 탭바 사용 방법에 대하여 알아봤습니다.

부족한 설명이지만, 조금은 이해 가셨나요?

틀린 내용이 있다면 언제든지 지적해 주시면 감사히 받겠습니다. 🫠
읽어주셔서 감사합니다 😃

다음번에는 내비게이션 바 설정 방법에 대하여 다뤄보도록 하겠습니다.

 

 

참고

 

Apple Developer Documentation

 

developer.apple.com

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!