본문 바로가기

학습 노트/iOS (2021)

110 ~ 113. Navigation Controller, Navigation Item & Navigation Bar, Customizing Navigation Controller and Toolbar

Navigation Controller

Navigation Controller는 가장 광범위하게 사용하는 Controller로,
iOS 설정 앱이 Navigation controler를 사용한 대표적이 예라고 할 수 있다.

Navigation Controller에서 원하는 항목을 선택하면 새로운 화면이 오른쪽에서 왼쪽으로 전환된다.
이런 전환 효과를 Push라고 부른다.

좌측 상단의 돌아가기 버튼을 누르면 이전의 화면이 왼쪽에서 오른쪽으로 전환된다.
이런 전환 효과를 Pop이라고 부른다.

Navigation Controller는 항상 Push와 pop으로 화면을 전환한다.

Navigation Controller는 Container Controll View에 해당한다.
따라서 실제 컨텐츠를 표현하는 Child View를 하나 이상 추가해야 한다.
그리고 Child View를 배열로 관리하는데, 이를 Navigation Stack이라고 부른다.

Navigation Stack에 가장 먼저 저장되는 View는 Root View Controller이다.
그리고 가장 마지막에 저장된 View Controller가 화면에 표시되고, 이를 Top View Controller라고 부른다.
새로운 View Controller를 표시하면 Navigation Stack에 추가되고, Top View Controller를 Pop 하면,
화면과 Navigation Stack에서 모두 제거된다.

Bavigation Controller의 상단에는 Navigation Bar가,
하단에는 Tool Bar가 표시된다.
Navigation Bar는 표시되는 게 기본 값이고, Tool Bar는 표시되지 않는 게 기본값이다.
이는 언제든지 설정을 변경할 수 있다.
이외의 나머지 영역들은 Top View Controller의 Root View가 표시된다.

Navigation Bar는 Top View Controller와 연관된 버튼과 Title을 표시한다.
Button은 왼쪽과 오른쪽에 표시될 수 있고, 왼쪽은 보통 뒤로 가기 버튼을 할당하나 커스텀이 가능하다.
Button은 UIBarButtonItem 인스턴스로 추가해야 한다.
즉, 일반 Button이나 Switch를 추가할 때에도 UIBarButtonItem으로 Wrapping 해 추가해야 한다.
중앙에는 Titlte이 효시되고 그 위엔 Prompt 문자열이 표시된다.

Title와 Button은 Navigation Item 객체에 저장한다.
모든 View Controller가 해당 객체를 가지고 있다.

Toolbar는 Top View Controller와 연관된 Button을 표시한다.
View Controller에 저장되어있는 Toolbar Item에 따라 알맞게 표시된다.

iOS 11부터 Large Title 모드가 추가되었다.
이를 활성화시키면 Title의 폰트 사이즈가 커지고, Button의 아래쪽에 표시되게 된다.
또한 Navigation Bar에 Search Bar를 추가할 수 있게 된다.

앱을 실행하면 Table View로 구현된 Root View Controller 씬이 표시된다.
Root View Controller 씬은 Navigation Controller에 Embed 되어있고,
Navigation Stack에 가장 먼저 추가된 Root View Controller이다.

여기서 항목을 선택하면 연결된 씬이 Navigation Stack에 Push 된다.

Navigation Cotroller 사용하기

사용할 씬은 위와 같다.
우선은 Navigation Controller에서 Segue를 추가해 씬을 전환해 보고,
이후에 코드로 구성해 본다.

Stroyboard로 구현하기

라이브러리에서 Navigation Controller를 추가하면 다른 Controller들과는 달리 두 개의 씬이 추가된다.
왼쪽이 Navigation Controller이고, 오른쪽이 Navigation Controller가 관리하는 Root View Contoller이다.
Navigation Controller가 표시될 때는 이 Root View Controller가 표시되게 된다.

Navigation Controller 자체에는 위와 같이 Root View가 존재하지 않아 View를 추가하고 화면을 구성할 수는 없다.

둘 사이에 존대하는 해당 Segue는 화살표가 가리키는 방향의 씬을 Root View Controller로 지정한다.

함께 생성된 씬을 하나 제거하고 새로운 씬을 생성했다.

그리고 둘을 Relationship Segue의 root view controller를 선택해 연결한다.

이후 원래의 씬에서 Present Modally로 Navigation Controller를 연결하면 최종적으로 위처럼 씬이 연결된다.

잘 보이지 않지만 First 씬에는 Navigation Bar가 존재한다.

반면 연결되지 않은 새로운 씬은 보이지 않는다.

이 둘을 Push로 연결하게 되면 위와 같은 모습이 되고, Navigation Bar가 표시된다.
또한 새로운 Navigation Controller의 Root View Controller인 First 씬과 달리 Back 버튼이 생겼다.
Navigation Stack에 추가되는 View Controller 중
Root View Controller를 제외한 모든 View Controller는 Back 버튼이 자동으로 표시된다.
또한 해당 버튼을 터치하면 이전 화면으로 돌아갈 수 있다.

같은 방식으로 여러 씬들을 연결했다.

Root View Controller에 해당하는 Firtst 씬이 처음 표시되고,
Segue 버튼을 터치하면 연결되어있는 Second 씬이 표시된다.
또한 왼쪽 상단의 Back 버튼을 터치하면 이전 씬인 First 씬으로 Pop 한다.

Code로 구현하기

Navigation Bar에 Back 버튼을 표시하는 것과 이전 화면으로 전환되는 기능은
Navigation Controller가 담당하므로 직접 구현할 필요가 없다.
하지만 Navigation View Controller와 Child View Controller를 생성하는 부분,
새로운 Child View Controller를 생성하는 부분은 직접 구현해야 한다.

씬들에는 본인들의 이름과 동일한 Custom Class가 연결되어있고,
Storyboard ID도 설정되어있다.
이를 사용해 해당 특정 씬을 식별하고, 코드로 구현할 수 있다.

//
//  NavigationControllerHostViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class NavigationControllerHostViewController: UIViewController {
	
	@IBAction func presentNavigationController(_ sender: Any) {
			
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}

Navigation Controller 씬의 Code 버튼은 NavigationControllerHostViewController 클래스 파일의
presentNavigationController 메소드와 연결되어있다.

@IBAction func presentNavigationController(_ sender: Any) {
	guard let rootVC = storyboard?.instantiateViewController(withIdentifier: "FirstViewController") else { return }
}

우선 Root View Controller를 생성한다.
instantiateViewController 메소드에 앞서 지정한 Storyboard ID를 전달한다.

@IBAction func presentNavigationController(_ sender: Any) {
	guard let rootVC = storyboard?.instantiateViewController(withIdentifier: "FirstViewController") else { return }
	let nav = UINavigationController(rootViewController: rootVC)
}

Navigation Controller는 UINavigationController 클래스로 구현되어있다.
UINavigationController 메소드를 사용하는데 코드에서 생성할 때는 rootViewController를 파라미터로 사용하는 메소드를 사용한다.
파라미터에 위에서 생성한 Root View Controller를 전달한다.

@IBAction func presentNavigationController(_ sender: Any) {
	guard let rootVC = storyboard?.instantiateViewController(withIdentifier: "FirstViewController") else { return }
	let nav = UINavigationController(rootViewController: rootVC)
	
	present(nav, animated: true, completion: nil)
}

이후 Navigation Controller를 Modal 방식으로 표시하도록 한다.

새로운 Child를 Push 하는 기능은 View Controller 클래스에서 개별적으로 구현해야 하므로,
First 씬에서 구현한다.

//
//  FirstViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class FirstViewController: UIViewController {

	@IBAction func pushSecond(_ sender: Any) {
		guard let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") else { return }
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}


}

View Controller를 생성하는 것 까지는 동일하지만,
새로운 Child를 Push 하는 것은 이전과 다르게 Navigation Controller에 접근해야 한다.
모든 View Controller가 해당 속성을 가지고 있다.

@IBAction func pushSecond(_ sender: Any) {
	guard let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") else { return }
	navigationController
}

View Controller가 Navigation Controller에 Embed 되어있다면 Navigation Controller를 반환하고,
그렇지 않다면 nil을 반환한다.

@IBAction func pushSecond(_ sender: Any) {
	guard let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") else { return }
	navigationController?.pushViewController(secondVC, animated: true)
}

해당 속성을 사용해 Navigation Contoroller에 접근하고 pushViewContoller 메소드를 호출한다.
표시할 View Controller를 전달하고, animated로 애니메이션 여부를 결정한다.

//
//  SecondViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class SecondViewController: UIViewController {
	
	@IBAction func pop(_ sender: Any) {
	
	}
	
	@IBAction func pushThird(_ sender: Any) {
		guard let thirdVC = storyboard?.instantiateViewController(withIdentifier: "ThirdViewController") else { return }
		navigationController?.pushViewController(thirdVC, animated: true)
	}
	
	@objc func addRightBtn() {
		let btn1 = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil)
		let btn2 = UIBarButtonItem(title: "Two", style: .plain, target: nil, action: nil)
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}


}

Code 버튼의 구현은 이전과 동일하다.
Pop 버튼은 이전 화면으로 돌아가는 기능을 가지며, Navigation Bar의 Back 버튼과 같은 기능을 하지만,
코드로 직접 구현해 본다.

Navigation Controller에서 제공하는 Pop 관련 메소드는 세 가지로,
popViewController 메소드는 View Controller를 Navigation Stack에서 제거하고 이전 화면으로 되돌린다.
popToView로 시작하는 메소드들은 돌아갈 화면을 직접 지정할 때 사용한다.

@IBAction func pop(_ sender: Any) {
	navigationController?.popViewController(animated: true)
}

간단하게 구현이 완료되었다.

//
//  ThirdViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class ThirdViewController: UIViewController {
	
	@IBAction func pushFourth(_ sender: Any) {
		guard let fourthVC = storyboard?.instantiateViewController(withIdentifier: "FourthViewController") else { return }
		navigationController?.pushViewController(fourthVC, animated: true)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}
//
//  FourthViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class FourthViewController: UIViewController {
	
	@IBAction func pushFifth(_ sender: Any) {
		guard let fifthVC = storyboard?.instantiateViewController(withIdentifier: "FifthViewController") else { return }
		navigationController?.pushViewController(fifthVC, animated: true)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
	
	}
}

나머지 씬들도 동일하게 구현한다.

의도한 대로 동작하는 것을 볼 수 있다.

마지막 Fifth 씬을 구현한다.
각각의 버튼을 누르면 코드와 Segue로 First 씬으로 이동하거나 Third 씬으로 이동하도록 구현한다.

Segue를 사용하는 경우 UnwindSegue를 사용해야 한다.

버튼을 끌어 Exit에 연결하려 하면 선택지가 나타나는데 지금은 관련이 없다.
UnwindSegue를 사용하려면 돌아가려는 Class에 연결할 메소드를 구현해야 한다.

class FirstViewController: UIViewController {
	
	@IBAction func unwindTo(_ unwindSegue: UIStoryboardSegue) {
		let sourceViewController = unwindSegue.source
		// Use data from the view controller which initiated the unwind segue
	}
	
	@IBAction func pushSecond(_ sender: Any) {
		guard let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") else { return }
		navigationController?.pushViewController(secondVC, animated: true)
	}
		
	override func viewDidLoad() {
		super.viewDidLoad()
	}


}

unwind의 기본 코드를 작성한다.
unwindTo의 뒤에 접미어를 붙여 unwindToFirst로 수정하고, 내용을 비운다.

@IBAction func unwindToFirst(_ unwindSegue: UIStoryboardSegue) {
	let sourceViewController = unwindSegue.source
	// Use data from the view controller which initiated the unwind segue
}

이후 다시 연결을 시도하면 새로 생성한 메소드가 표시되는 것을 확인할 수 있다.

//
//  FifthViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class FifthViewController: UIViewController {
	
	@IBAction func popToRoot(_ sender: Any) {
		navigationController?.popToRootViewController(animated: true)
	}
	
	@IBAction func popToThird(_ sender: Any) {
	}
	
	

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


}

Fifth 씬의 각각의 Code 버튼은 코드에 Action으로 연결되어있다.
popToRoot 버튼은 opoToRootViewController 메소드를 사용해 Root View로 이동시킨다.

두 버튼 모두 제대로 작동하는 것을 볼 수 있다.
사이에 존재하는 씬들은 모두 전환 효과에서 제외되도록 Navigation Controller가 알아서 정리한다.
남은 버튼도 비슷하게 구현한다.

//
//  ThirdViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class ThirdViewController: UIViewController {
	
	@IBAction func unwindToThird(_ unwindSegue: UIStoryboardSegue) {
	}
	
	@IBAction func pushFourth(_ sender: Any) {
		guard let fourthVC = storyboard?.instantiateViewController(withIdentifier: "FourthViewController") else { return }
		navigationController?.pushViewController(fourthVC, animated: true)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}

Third 씬의 클래스 파일에서 unwind 메소드를 추가한 뒤,
Segue 버튼을 Exit에 연결해 해당 메소드와 연결한다.

//
//  FifthViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class FifthViewController: UIViewController {
	
	@IBAction func popToRoot(_ sender: Any) {
		navigationController?.popToRootViewController(animated: true)
	}
	
	@IBAction func popToThird(_ sender: Any) {
		guard let thirdVC = navigationController?.viewControllers.first(where: { $0 is ThirdViewController }) else { return }
		navigationController?.popToViewController(thirdVC, animated: true)
	}

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

Fifth 씬의 남은 Action에도 코드로 같은 기능을 구현한다.
popToViewController 메소드를 사용하는데, 여기엔 대상 인스턴스가 필요하므로,
present때와 마찬가지로 인스턴스를 생성한 뒤 전달한다. 이번엔 navigationContoller의 viewControllers 속성에 접근해
Navigation Stack에서 해당하는 View Controller를 찾아 인스턴스를 생성한다.

두 버튼 모두 문제없이 동작한다.

 

Navigation Item & Navigation Bar

Navigation Bar는 Navigation Stack이 업데이트될 때마다 함께 업데이트된다.
Navigation Bar에 표시될 버튼과 Title은 Child View Controller에서 개별적으로 저장한다.
Navigation Item에 원하는 항목을 저장하면 Top View Contoller로 지정되었을 때 Navigation Bar에 표시된다.

이전에 사용했던 씬을 그대로 사용한다.
Navigation Bar에 Title을 표시하도록 한다.
표시할 Title은 View Controller에 속한 Navigation Item에 저장한다.

Storyboard에서 구현하기

First 씬은 Navigation Controller의 Root View Controller이다.

Navigation Bar를 선택하면 Attribute Inspector에서 Navigation Item을 편집할 수 있다.

Title에 First를 입력하면 씬에서 Title을 확인할 수 있다.

Navigation Bar에 Title과 Button을 동시에 추가하기 위해서는 위와 같은 방법을 사용한다.
만약 Navigation Item이 존재하지 않는다면 라이브러리에서 찾아 추가할 수 있다.

Code로 구현하기

//
//  FourthViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/26.
//

import UIKit

class FourthViewController: UIViewController {

	@IBAction func pushFifth(_ sender: Any) {
		guard let fifthVC = storyboard?.instantiateViewController(withIdentifier: "FifthViewController") else { return }
		navigationController?.pushViewController(fifthVC, animated: true)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
		navigationItem.title = "Fourth"
	}
}

UIViewController에는 NavigationItem 속성이 존재한다.
이 속성에 접근할 때 자동으로 인스턴스가 생성되므로 별도로 생성할 필요도 없다.
원하는 속성에 접근해 위와 같이 바로 설정할 수 있다.

씬에 따라 적절한 Title이 표시되는 것을 확인할 수 있다.

Button 추가하기

Storyboard로 추가하기

라이브러리에서 Bar Button Item을 찾아 First 씬의 왼쪽 상단에 추가한다.

Bar Button Item은 Navigation Item 이하의 Left Bar Button Items에 추가되었다.

해당 버튼이 지금처럼 Custom으로 되어있으면 아래에서 이름과 이미지 등을 직접 설정한다.

하지만 옵션 중 하나를 선택하게 되면 각각의 고유한 스타일로 설정된다.

이를테면 Add는 '+' 모양으로 변경되고, Cancel은 Cancel로 그 모습이 바뀐다.
코드에서 해당 버튼의 이벤트를 처리할 때는 Target Action 메커니즘을 사용한다.
다른 버튼과 동일하게 outlet이나 Action으로 연결한다.

오른쪽에는 Bar Button과 Switch를 하나씩 추가한다.
Navigation Bar에 추가되는 모든 View는 항상 Bar Button Item에 Embed 되어 추가된다.

switch가 Bar Button Item에 Embed 되어있다.
지금처럼 Storyboard에서 추가하면 자동으로 진행되지만 코드로 진행하면 직접 Embed 해 줘야 한다.

Code로 추가하기

@IBAction func switchChanged(_ sender: UISwitch) {
	switch sender.isOn {
	case true:
		navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
		
		if var list = navigationItem.rightBarButtonItems {
			let btn = UIBarButtonItem(title: "Item", style: .plain, target: nil, action: nil)
			list.append(btn)
			navigationItem.rightBarButtonItems = list
		}
	case false:
		navigationItem.leftBarButtonItem = nil
		
		let list = navigationItem.rightBarButtonItems?.dropLast()
		navigationItem.rightBarButtonItems = Array(list!)
		
	}
}

Switch의 isOn 값에 따라 분기해 버튼을 표시하고 제거한다.
isOn이 true인 경우 leftBarButtonItem에 UIBarButtonItem을 추가한다.
이때 사용하는 스타일이 기본으로 제공되는 'add'이기 때문에 barButtonSystemItem을 파라미터로 사용하는 메소드를 사용한다.
RightBarButtonItem에는 스위치 본인의 좌측에 "Item" 버튼을 추가한다.
BarButtonItem이 여러 개인 경우 BarButtonItems 속성에서 배열로 관리되므로,
RightBarButtonItems를 바인딩 한 다음 수정해 업데이트한다.
이때 배열에 저장된 역순으로 왼쪽부터 표시되므로 스위치의 앞이 아닌 뒤에 버튼을 추가해야 함에 주의하자.

제거할 때는 LeftBarButtonItem을 nil로 비워주거나,
RightBarButtonItems를 수정해 업데이트한다.

의도한 대로 문제없이 작동된다.

Back 버튼이 있는 상태에서 Button 추가하기

Storyboard에서 추가하기

다음 씬부터는 Root View Controller가 아니기 때문에 Back 버튼이 좌측 상단에 자동으로 추가된다.
따라서 LeftBarButton을 조금 다른 방식으로 추가해야 한다.

Navigation Controller가 Left Bar Button을 추가하는 규칙은 
사용자가 직접 추가하면 Back Button 대신 표시된다.
만약 이전 View Controller가 Back Bar Button Item을 가지고 있다면 이것을 표시한다.
사용자가 추가한 버튼도 없고, 이전 View Controller에 Back Bar Button도 없고,
Root View Controller가 아니라면 Back 버튼을 표시한다.
Back 버튼의 Tilte은 이전 화면의 Title로 설정되는 것이 기본값이다.
하지만 공간이 부족하다면 "Back"으로 대체된다.

Bar Button Item을 Navigation Bar 왼쪽에 추가한다.
이렇게 되면 Back 버튼을 자동으로 구현하지 않기 때문에 이전으로 돌아가는 기능은 직접 구현해야 한다.

둘 다 사용하고 싶다면 Navigation Item에서 Left Items Supplement 옵션을 선택해 준다.

그러면 다시 Back  버튼이 생긴다.

Code로 추가하기

override func viewDidLoad() {
	super.viewDidLoad()
	
	navigationItem.leftItemsSupplementBackButton = true
	navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addRightBtn))
}

viewDidLoad에서 leftItemsSupplementBackButton 속성을 true로 설정하고,
leftBarButtonItem을 추가한다.
이때 이미 작성해둔 addRightBtn 메소드를 연결한다.
addRightBtn은 RightBarButton을 추가하는 메소드이다.

@objc func addRightBtn() {
	let btn1 = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil)
	let btn2 = UIBarButtonItem(title: "Two", style: .plain, target: nil, action: nil)
	
	let sw = UISwitch()
	let switchItem = UIBarButtonItem(customView: sw)
	
	navigationItem.setRightBarButtonItems([switchItem, btn1, btn2], animated: true)
}

스위치를 추가할 때는 UIBarButtonItem으로 Embed 해야 하는 것을 명심하자.
생성한 BarButtonItem과 switch를 setRightBarButtonItems에 배열로 전달한다.
해당 메소드는 rightBarButtonItems에 단순히 배열로 전달하는 것과 다르게 애니메이션 효과를 함께 적용한다.

의도한 대로 잘 동작하는 것으로 볼 수 있다.

Back 버튼 title 변경하기

Storyboard에서 변경하기

Back Button Title을 기본값으로 이전 View Contoller의 Title을 가지고 있다.
따라서 공간이 부족하다면 "Back"으로 표시된다.
해당 문자열은 언어에 따라 적절한 값으로 번역되어 표시된다.
따라서 원하는 Title을 설정하고 싶다면 이전 화면에서 설정해야 한다.
즉, Second 씬에서 표시되는 Back Button의 Title은 First 씬에서 설정해야 한다.

First 씬의 Navigation Item에서 Back Button을 원하는 값으로 변경하면

Second 씬의 Back 버튼이 해당 값으로 바뀌게 된다.

Code에서 변경하기

override func viewDidLoad() {
	super.viewDidLoad()
	
	navigationItem.backBarButtonItem?.title = "Peace"
}

First 씬의 viewDidLoad에서 backButtonTitle 속성을 원하는 값으로 설정하면 된다.

의도한 값으로 변경됐다.

 

Customizing Navigation Controller

사용할 씬은 Modal 방식으로 Table View Controller와 연결되어있다.
해당 씬은 Navigation Controller에 Embed 하고 싶다면 새로운 Navigation Contoller를 추가하고,
Root View Controller로 지정한 다음 Segue를 다시 연결해야 한다.
Interface Builder는 이러한 상황에서 사용할 수 있는 메뉴를 제공한다.

대상으로 사용할 씬을 선택하고,

상단 메뉴에서 Editor > Embed in > Navigation Controller를 선택한다.

그러면 자동으로 Navigation Controller를 추가하고, Segue로 연결한다.

씬의 Navigation Bar를 선택하고, Title을 설정해 준다.

Navigation Controller의 Attribute Inspector는 다양한 설정을 제공한다.

  • Shows Navigation Bar
    Navigation Bar의 표시 여부를 결정한다.
  • Shows Toolbar
    Toolbar의 표시 여부를 결정한다.

기본값은 Navigation Bar를 표시하고, Toolbar는 표시하지 않는다.
만약 Toolbar를 표시하고자 한다면 Shows Toolbar 옵션을 활성화한다.

만약 Navigation Bar를 원하는 디자인으로 직접 구현한다면
두 가지를 모두 설정 해제하고 기본 Navigation Ber를 숨길 수 있다.
이를 숨긴다고 해서 Push, Pop 전환 효과를 사용하지 못하는 것은 아니다.

  • On Swipe, On Tap
    Bar를 Toggle 하는 시점을 결정한다.

On Swipe 방식은 Sheet 형식의 Modal에서는 확인할 수 없다.
하지만 On Tap 방식은 화면을 터치하면 Navigation Bar가 숨겨지는 것을 확인할 수 있다.

  • When Vertically Compact
    활성화하면 iOS가 가로모드일 때 Navigation Bar를 숨긴다.

가로모드의 경우 높이에 제약이 심하게 생기기 대문에 이를 사용해 공간을 확보하는 것이 가능하다.
단, 이 경우 On Tap이나 On Swipe를 사용하거나 다른 방식을 통해 Navigation Bar를 표시할 수 있는 방법을 제공해야 한다.

Navigation Bar의 시각적인 변경도 가능하다.

Navigation Bar는 두 가지의 기본 스타일을 제공한다.
기존에는 반투명한 배경을 두고 흰색이나 검은색으로 강조했지만,
최신 버전에는 폰트 색만으로 이를 구분한다.

  • Translucent
    Navigation Bar의 배경을 투명하게 하거나 반투명하게 바꾼다.
  • Prefers Large Titles
    iOS 11부터 추가된 Large Title Navigation Bar를 사용한다.
    따라서 이전 버전에서는 기본 모드로 표시된다.
    Interface Builder에서는 별 다른 작업이 필요하지 않지만,
    코드에서 사용할 때는 버전에 따라 분기하도록 작성해야 한다.

    UINavigationController 클래스를 Subclassing 하고,
    원하는 코드를 구현한 다음 Navigation Controller와 연결한다.

    Root View Controller 클래스에서 구현한다.
    이 경우 별도의 파일을 추가할 필요가 없기 때문에 조금 더 간단하다.
//
//  CustomizingViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

class CustomizingViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}

}

Customizing 씬과 연결된 코드의 viewDidLoad에서 코드를 작성한다.

//
//  CustomizingViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

class CustomizingViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		if #available(iOS 11.0, *) {
			navigationController?.navigationBar.prefersLargeTitles = true
		}
	}
}

navigationBar에 접근하기 위해서는 navigationController에 먼저 접근해야 한다.
prefersLargeTitles 속성을 true로 바꿔 주는데,
iOS 11 이하를 지원한다면 위처험 #available을 사용해 코드의 버전에 따라 분기해 줘야 한다.
Large Title을 활성화하면 Navigation Bar가 항상 Large Title로 표시된다.

Navigation Bar의 Attribute Inspector를 다시 보면 Large Title 속성이 Automatic으로 설정되어 있다.
해당 설정은 개별 화면에서 Large Title 모드를 설정한다.

  • Automatic
    이전 화면의 설정을 상속한다.
  • Always
    이전 화면의 설정에 관계없이 항상 Large Title 모드를 사용한다.
  • Never
    이전 화면의 설정에 관계없이 항상 기본 모드를 사용한다.
    만약 다음에 표시되는 씬이 Automatic으로 설정되어있다면 해당 씬도 기본 모드로 표시된다.
if #available(iOS 11.0, *) {
	navigationController?.navigationBar.prefersLargeTitles = true
	navigationItem.largeTitleDisplayMode = .automatic
}

코드에서는 navigationItem을 통해 largeTitleDisplayMode 속성을 설정한다.
둘은 동일하게 Navigation Title을 설정하는 코드지만
활성화를 할 때는 Navigation Controller의 navigationBar에 접근해 설정하고,
동작 방식을 설정할 때는 navigationItem에 접근해 설정 한다.

Navigation Bar의 배경색은 Bar Tint로 설정한다.
해당 설정을 변경하게 되면 기본 스타일은 무시되게 된다.
또한 해당 설정은 Title 등의 tint 색에는 영향을 주지 않는다.

씬에 Bar Button Item을 추가하고,
Bar Button Item의 Tint 색상을 변경하면 해당 버튼의 색상만 변경되게 된다.

Navigation Controller에서 가장 아래쪽에 있는 Tint를 변경하게 되면,
Child View Controller에서도 같은 Tint 색을 사용하게 된다.

asset에 추가되어 있는 shadow 이미지는 너비가 31px이다.
해당 이미지를 Navigation Bar에 설정하면 너비가 Navigation Bar에 맞게 수정된다.

Appearance를 Standard로 바꾸고,
Shadow를 설정하면 오른쪽 사진과 같이 Navigation Bar 아래에 그림자가 추가된다.

View Controller를 하나 생성하고 Right Bar Button과 Show 방식으로 Segue 연결한다.
이번엔 자동으로 생긴 Back Button을 수정해 본다.

asset 파일로 화살표가 추가되어있다.
back 은 기본 상태의 이미지고, back-mask는 마스킹용 이미지이다.

Navigation Controller의 Navigation Bar를 선택하고,
Standard Bar Button Appearances에서 Back Button을 Custom으로 변경하고,
Image를 변경할 수 있다.
State Config를 사용하여 Highlighted, Normal, Disabaled, Focus 각각에 맞게 설정할 수 있다.

이외에도 다양한 부분을 변경할 수 있다.

Title 속성을 코드로 변경하려는 경우 속성을 Dictionary에 저장해야 한다.
또한 Key 타입을 NSAttributeString으로 설정하고, Value를 Any로 설정해야 한다.

Navigation Controller의 Custom은 iOS의 버전이 올라갈수록 제약이 늘어가고 있다.
이후 강의에서는 제외될 내용이라고 하니 참고만 하자.

 

Tool Bar

Navigation Controller의 Attribute Inspector에서 Shows Toolbar를 활성화하는 것으로
간단하게 Toolbar를 사용할 수 있다.
UINavigationController가 제공하는 API를 통해 이를 toggle 하는 것도 가능하다.

RootView의 구조를 보면 Navigation Item이 존재하지만,
Toolbar에 추가되는 Item들은 Navigation Item에 추가되지 않는다.

라이브러리에서 Bar Button Item을 찾아 씬에 추가하면
오른쪽과 같이 Toolbar Items가 새롭게 추가된다.
Navigation Controller는 Top View Controller가 저장하고 있는 Toolbar Item을 Toolbar에 표시한다.
Navigation Item과 마찬가지로 전환이 발생할 때마다 자동으로 업데이트된다.

추가된 Bar Button Item은 왼쪽을 기준으로 정렬된다.

라이브러리에서 Flexible Space Bar Button Item을 찾아 기존의 버튼 왼쪽에 추가하면,
오른쪽 그림과 같이 원래의 버튼이 오른쪽으로 밀려나게 된다.
Flexable Space Button은 실제로 UI를 출력하지 않고, Item 배치와 간격을 조정한다.

오른쪽에 하나 더 추가하면 추가했던 버튼은 중앙에 위치하게 된다.

//
//  SecondToolbarViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

class SecondToolbarViewController: UIViewController {
	@IBAction func ToggleAction(_ sender: Any) {
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
	
	}

}

Second Toolbar 씬에는 코드로 Toolbar Item을 추가해 본다.
Toolbar는 Toolbar Item을 추가하기 위해 속성에 접근하면 자동으로 생성된다.

//
//  SecondToolbarViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

	class SecondToolbarViewController: UIViewController {
		@IBAction func ToggleAction(_ sender: Any) {
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
	}

}

우선 flaxibleSpaceItem을 생성한다.

//
//  SecondToolbarViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

class SecondToolbarViewController: UIViewController {
	@IBAction func ToggleAction(_ sender: Any) {
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
		let addItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
		let shareItem = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil)
	}

}

추가할 버튼도 몇 개 생성한다.

//
//  SecondToolbarViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

class SecondToolbarViewController: UIViewController {
	@IBAction func ToggleAction(_ sender: Any) {
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
		let addItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
		let shareItem = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil)
		
		setToolbarItems([flexibleSpaceItem, addItem, shareItem, flexibleSpaceItem], animated: true)
	}

}

setToolbarItems 메소드를 통해 표시할 Item을 추가한다.
이때 추가하는 Item들은 배열로 전달되어야 하고, 두 번째 파라미터인 animated를 통해 애니메이션 여부를 설정할 수 있다.

//
//  SecondToolbarViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/28.
//

import UIKit

class SecondToolbarViewController: UIViewController {
	@IBAction func ToggleAction(_ sender: Any) {
		let hidden = navigationController?.isToolbarHidden ?? false
		navigationController?.setToolbarHidden(!hidden, animated: true)
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
		let addItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
		let shareItem = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil)
		
		setToolbarItems([flexibleSpaceItem, addItem, shareItem, flexibleSpaceItem], animated: true)
	}

}

씬의 우측 상단에 존재하는 Toggle 버튼은 action으로 연결되어있다.
해당 메소드에서 navigationController의 isToolbarHidden 속성을 통해 현재 Toolbar의 상태를 받아온다.
이후 setToolbarHidden 속성을 통해 설정할 값을 전달하고, 두 번째 파라미터로 애니메이션 여부를 설정한다.

설정한 항목들이 정상적으로 동작한다.