본문 바로가기

학습 노트/iOS (2021)

114 ~ 115. Tab Bar Controller and Tab Bar Controller Customizing

Tab Bar Controller

Tab UI를 구현할 때 사용한다.

Tab Bar Controller는 광범위하게 사용되고 있는 Controller이다.
Tab Bar Controller는 화면 하단의 Tab Bar에는 가지고 있는 Child의 수만큼 Tab Bar Item이 표시된다.
해당 Item을 선택하면 나머지 공간에 각각에 해당되는 Child가 표시된다.

Tab Bar에 표시할 수 있는 Item의 수는 iPhone Portrait에서 5개 까지이고,
이를 초과하면 More Item으로 이를 축약되고, 해당 Item을 선택하면
나머지를 선택하거나 순서를 변경할 수 있는 More Navigation이 표시된다.

모든 Child는 연관된 Tab Bar Item을 가지고 있다.
Tab Bar Item에 title, 이미지 등을 저장하면 Tab Bar에 표시된다.

위의 사진처럼 Ttitle이 Image 아래쪽에 표시되는 Tab Bar를 Regular Tab Bar라고 부른다.

지금 사진처럼 Iamge와 title이 나란히 표시되는 Tab Bar를 Compact Tab Bar라고 부른다.

사진 앱과 같이 Navigation Controller와 Tab Bar Controller를 함께 사용하는 경우도 있는데,
이런 경우 Child를 Navigation Controller로 Emebed 하는 방식으로 구현한다.
반대로 Navigation Controller를 Tab Bar Controller에 Embed 하는 방식은 좋지 못하다.

사용할 씬은 위와 같다.
우선 Storyboard에 Tab Bar Controller를 추가한다.

라이브러리에서 Tab Bar Controller를 찾아 추가한다.
이때 기본적으로 두 개의 Child View가 추가된다.
이들은 서로 Relation Segue로 연결되어있다.
따라서 새 Tab을 추가하고 싶다면 새 View Controller를 생성하고 Relationship Segue로 연결하면 된다.

새 View를 추가하고 Relationship Segue로 연결하면 Tab Bar에 자동으로 추가되고,
드래그를 통해 순서를 바꾸는 것도 가능하다.

원래의 씬과 Tab Bar Controller를 Modally로 연결한다.
이후 현재 Tab Bar와 연결되어있는 Child를 삭제하고,
미리 생성해둔 View Controller 중 5개만 연결하도록 한다.

이후 결과를 확인해 보면 해당 Tab을 선택할 때마다 각각의 씬으로 전환되는 것을 확인할 수 있다.
이때 애니메이션이나 전환 효과는 존재하지 않는데, 이를 적용하고 싶다면 Custom Transition을 직접 구현해야 한다.
Tab Bar는 현재 Title이 Item으로 고정되어있고, Image도 표시되지 않는다.

각각의 씬들은 Item을 가지고 있다.
이것이 Tab Bar Item이다.
View Controller와 Tab Bar Controller를 연결하면 자동으로 추가되고,
이를 수정해 Tab Bar를 설정할 수 있다.

Tab Bar Item의 이미지와 Title 등을 Attribute Inspector에서 설정할 수 있다.

기본적으로 Custom으로 설정되어있는 System Item은 Tab Bar의 이미지를 설정하는 옵션이다.
이를 선택하면 여러 시스템 아이콘을 사용할 수 있다.

예를 들면 Favorite을 선택하면 별 모양 이미지와 함께 Title이 Favorite으로 변경되고,
Contacts를 선택하면 프로필 이미지와 함께 Contacts로 변경되는 것이다.
즉 System Item은 이미지와 Title이 고정되어있다.

연결된 모든 씬들에 Tab Bar Item을 System Item으로 설정했다.

//
//  FirstSceneViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/29.
//

import UIKit

class FirstSceneViewController: UIViewController {
	
	@IBAction func SecondTabAction(_ sender: Any) {
	}
	@IBAction func ThirdTabAction(_ sender: Any) {
	}
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}

}

첫 번째 씬과 연결된 코드는 위와 같다.

코드에서 Tab Bar Item을 설정하기 위해서는 UIViewController에 선언되어있는 tabBarItem 속성을 사용한다.
해당 속성을 통해 Tab Bar Item의 Title, Image, Badge를 설정한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	tabBarItem.title = "First"
	tabBarItem.badgeValue = "Hot"
}

코드에서 Title을 변경하고, Badge를 설정했다.
System Item으로 지정되어있기 때문에 Badge를 제외한 Title과 Iamge는 코드를 통해 설정할 수 없다.
Storyboard의 설정을 무시하고 Code에서 설정해야 한다면 새로 생성하고 할당해야 한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	tabBarItem.title = "First"
	tabBarItem.badgeValue = "Hot"
	
	tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0)
}

Tab Bar Item을 코드에서 새로 생성해 할당했다.
따라서 새 Tab Bar Item으로 교체됐지만 처음 First 씬에 진입했을 때 Tab Bar가 Highlight 되지 않는다.
이는 Tab Bar Controller가 첫 번째 Tab Bar Item을 선택한 뒤 해당 Item을 교체했기 때문이다.
따라서 viewDidLoad가 아닌 더 이른 시점에 교체해야 한다.

override func awakeFromNib() {
	tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0)
}

Tab Bar를 새로 할당하는 시점을 awakeFromNib로 옮겼다.
이후엔 정상적으로 선택된 상태로 표시되는 것을 확인할 수 있다.

나머지 씬들을 모두 연결하고, Tab Bar Item을 System Item으로 각각 설정한다.

Tab Bar Item의 수가 표시할 수 있는 한도인 5개를 초과하게 되면
More Item으로 이들을 표시한다.
More Item을 선택하면 표시하지 못한 Tab들이 More Navigation Controller로 표시된다.

Tab Bar Controller를 사용할 때는 최대한 적은 양의 Tab을 사용해 제한을 넘지 않도록 하는 것이 좋다.
하지만 지금처럼 제한을 넘겨서 사용해야 한다면 중요도 순으로 정렬해 사용하는 것이 좋다.

More Navigation Controller의 우측 상단 Edit을 터치하면
Tab Bar Customization Sheet를 표시할 수 있다.
표시되지 않은 Item을 드래그 해 해당 위치에 대신 표시할 수 있고,
위치를 조정할 수도 있다.

기본적으로 모든 Tab은 편집 대상에 포함된다.
만약 위치를 고정해야 한다면 편집 대상에서 제외시켜야 한다.

예외처리를 위해 새로운 파일을 생성한다.
UITabBarController를 상속하도록 생성한다.

//
//  CustomTabBarController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/29.
//

import UIKit

class CustomTabBarController: UITabBarController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		customizableViewControllers
	}

}

UITabBarController는 customizableViewControllers가 반환하는 Tab Bar Item을 편집 대상으로 삼는다.
따라서 반환하기 전에 편집 대상에서 제외하고자 하는 Item을 배열에서 제거해 반환해야 한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	customizableViewControllers = Array(customizableViewControllers!.dropFirst(2))
}

맨 앞에 두 개를 배열에서 제거해 편집 대상에서 제외했다.
해당 파일은 Tab Bar Controller의 Custom Class로 등록한다.

그러면 Tab Bar Customization Sheet에서 맨 앞의 두 개의 Tab이 제외된다.
물론 드래그로 순서를 바꾸는 것도 불가하다.

코드를 사용해 Tab 선택하기

//
//  FirstSceneViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/29.
//

import UIKit

class FirstSceneViewController: UIViewController {
	
	@IBAction func SecondTabAction(_ sender: Any) {
	}
	@IBAction func ThirdTabAction(_ sender: Any) {
	}
	
	override func awakeFromNib() {
		tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		tabBarItem.title = "First"
		tabBarItem.badgeValue = "Hot"
	}

}

First Scene에는 Button이 두 개가 존재하고, 이는 FirstSceneViewController 클래스에 avtion으로 연결되어있다.
해당 버튼을 사용해 각각 다른 방법으로 Tab을 선택한다.

선택할 View Controller를 지정하기

@IBAction func SecondTabAction(_ sender: Any) {
	guard let secondChild = tabBarController?.children[1] else {
		return
	}
	tabBarController?.selectedViewController = secondChild
}

Child View Controller에서 Tab Bar에 접근하기 위해선 tabBarController를 통해 접근해야 한다.
selectedViewController에 선택할 View Controller를 전달해야 하므로,
Child View Controller를 저장하는 children 배열에서 대상에 해당하는 View Controller를 바인딩 해 전달한다.

선택할 Tab을 Index로 지정하기

@IBAction func ThirdTabAction(_ sender: Any) {
	tabBarController?.selectedIndex = 2
}

간단하게 selectedIndex 속성에 해당하는 선택할 Tab의 Index를 전달하면 된다.

의도한 대로 잘 작동한다.
선택할 Tab과 연관된 View Controller 참조를 이미 가지고 있거나,
적확한 Index를 파악할 수 없을 때 첫 번째 방법을 사용한다.
이외에는 두 번째 방법을 사용한다.

Tab 선택 금지하기

Tab 선택을 금지하기 위해선 Delegate 패턴을 활용해야 한다.

//
//  CustomTabBarController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/29.
//

import UIKit

class CustomTabBarController: UITabBarController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		customizableViewControllers = Array(customizableViewControllers!.dropFirst(2))
		
		delegate = self
	}
	
}

extension CustomTabBarController: UITabBarControllerDelegate {
	func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
		return true
	}
}

extension으로 UITabBarControllerDelegate를 채용하고,
tabBarController(shouldSelect:) 메소드를 구현한다.
해당 메소드는 Tab이 선택될 때마다 호출되고, true를 반환하면 실제로 선택되고, false를 반환하면 선택되지 않는다.

extension CustomTabBarController: UITabBarControllerDelegate {
	func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
		
		if let second = tabBarController.viewControllers?[1] {
			return viewController != second
		}
		
		return true
	}
}

선택된 Tab에 대한 정보는 두 번째 파라미터인 viewController로 전달된다.
따라서 tabBarController의 두번째 Controller와 동일한 View Controller라면 false를 반환해 선택을 금지한다.

의도한 대로 두 번째 Tab은 선택되지 않는다.
하지만 코드를 통해 선택하는 것은 가능하다.
코드로 선택하는 것까지 막고자 한다면 API에 직접 조건문을 추가해야 한다.

func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {

}

UITabBarControllerDelegate 프로토콜에서 호출할 수 있는
tabBarController(didSelect:)메소드는 Tab이 선택된 직후 호출된다.
여기서도 마찬가지로 두 번째 파라미터로 전달되는 View Controller로 선택된 View에 접근할 수 있다.
해당 메소드에서 조건에 따라 선택을 초기화하도록 구현하면 된다.

Code로 Tab Bar Controller 생성하기

//
//  TapBarViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/29.
//

import UIKit

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

}

Tab Bar Controller 씬의 Code 버튼은 코드에 presentTabBarController 메소드로 연결되어있다.
Tab Bar Controller를 생성하려면 하나 이상의 Child가 존재해야 한다.
이미 생성되어있는 씬의 Storyboard ID를 사용해 생성해 본다.

@IBAction func presentTabBarController(_ sender: Any) {
	let first = storyboard!.instantiateViewController(withIdentifier: "FirstScene")
	let second = storyboard!.instantiateViewController(withIdentifier: "SecondScene")
	let third = storyboard!.instantiateViewController(withIdentifier: "ThirdScene")
	
	let tbc = UITabBarController()
}

Storyboard ID를 사용해 인스턴스를 생성하고, UITabBarController 생성자를 사용해 Tab BAr Controller를 생성한다.
해당 생성자는 배열로 Child 배열을 받지 않기 때문에 생성 후 viewControllers 속성에 Child 배열을 저장해야 한다.

@IBAction func presentTabBarController(_ sender: Any) {
	let first = storyboard!.instantiateViewController(withIdentifier: "FirstScene")
	let second = storyboard!.instantiateViewController(withIdentifier: "SecondScene")
	let third = storyboard!.instantiateViewController(withIdentifier: "ThirdScene")
	
	let tbc = UITabBarController()
	tbc.viewControllers = [first, second, third]
	
	present(tbc, animated: true, completion: nil)
}

배열로 전달하고 새 Tab Bar Controller와 Modal 방식으로 연결한다.
이때 setViewControllers(animated:) 메소드를 사용하면 애니메이션을 사용한 동적인 Tab 추가를 구현할 수 있다.

의도한 대로 세 개의 Tab을 가지는 Tab Bar Controller가 생성되었고, 정상적으로 작동한다.
Tab Bar Item의 아이콘과 이름이 이전에 사용하던 것과 동일한 이유는
Storyboard에서 구현한 View Controller의 정보를 그대로 사용했기 때문이다.

이를 변경하고자 한다면 인스턴스를 생성하고 새로운 Tab Bar Item을 설정해야 한다.

@IBAction func presentTabBarController(_ sender: Any) {
	let first = storyboard!.instantiateViewController(withIdentifier: "FirstScene")
	first.tabBarItem = UITabBarItem.init(tabBarSystemItem: .favorites, tag: 0)
	let second = storyboard!.instantiateViewController(withIdentifier: "SecondScene")
	let third = storyboard!.instantiateViewController(withIdentifier: "ThirdScene")
	
	let tbc = UITabBarController()
	tbc.viewControllers = [first, second, third]
	
	present(tbc, animated: true, completion: nil)
}

이렇게 인스턴스에 접근해 tabBarItem 속성에 새로운 UITabBarItem을 생성해 저장하면 된다.

 

Tab Bar Controller Customizing

Tab Bar Customizing은 두 가지 주제로 구분된다.
Tab Bar의 개별 Item을 Customizing 하는 것과, Tab Bar 자체를 Customizing 하는 것이다.

이전에 사용했던씬들을 그대로 사용한다.

Tab Bar Item Customizing

씬의 Tab Bar를 선택하고 Attribute Inspector를 확인해 보면 Customizing이 가능한 여러 속성들이 존재한다.

Badge

Badge에 입력하는 내용이 Tab Bar Icon의 우측 상단에 표시된다.
주로 Tab과 연결된 화면에 확인하지 않은 새로운 정보가 있다는 것을 알릴 때 사용한다.
해당 속성에 저장할 수 있는 문자열에 제한은 없지만 현실적으로 3자 이하가 적당하다.
더 길어지게 되면 빨간 배경을 벗어나고, 실행했을 때 정상적으로 표시되지 않는다.

Bage의 Backgorund Color는 빨간색이 기본이다.

바로 아래에 있는 속성을 설정해 배경색을 바꿀 수 있다.

System Item

해당 속성을 사용해 자주 사용하는 Item을 자동으로 사용하거나,
Custom을 선택해 Image와 Title을 직접 설정할 수도 있다.

Selected Image

선택됐을 때의 Image를 변경하고 싶다면 해당 속성을 통해 설정할 수 있다.

Title Position

Tab Bar Item의 Title은 기본적으로 가운데에 표시된다.
해당 속성을 Custom Offset으로 변경하고, 원하는 위치로 옮길 수 있다.
단, 너무 큰 값을 입력하면 다른 Item과 겹칠 수 있어 주의해야 한다.

Title

Tab Bar Item의 Title을 설정한다.
Badge와 마찬가지로 설정할 수 있는 제한은 없지만 10글자 이내로 작성하는 것이 좋다.
마찬가지로 너무 길어질 경우 다른 Item과 겹칠 수 있다.

Image & Landscape

Item의 Image를 설정하는 옵션이다.
지금 표시되고 있는 파란색 사각형은 실제 표시되는 이미지가 아닌 Placeholder이다.
사용할 수 있는 권장 사이즈는 다음과 같다.

원형   사각형
25 * 25 Regular Tab Bar 23 * 23
18 * 18 Compact Tab Bar 17 * 17

이에서 벗어난 사이즈의 이미지를 사용하면 제대로 Resize 되지 않기 때문에 반드시 지켜야 한다.

asset에 사용할 이미지들을 추가한다.
Tab Bar에 표시되는 이미지는 항상 Template 이미지로 표시된다.
따라서 실제 이미지를 제외한 나머지 영역이 투명한 이미지를 사용해야 한다.
원본 그대로를 사용해야 한다면 Image Rendering Mode를 설정해야 한다.
이미지들 중 Calendar, Health, Setting 이미지는 위에서 설명한 권장 사이즈에 충족한다.
News 이미지는 단일 스케일로 설정되어있는 PDF Vector 이미지이다.
Selection Image는 Connect 이미지로 사용된다.
나머지 Setting 이미지는 32, 64의 크기를 가지고 있다.
해당 이미지들과 벡터 이미지를 비교해 본다.

Image는 일반 상태에서 표시할 Image이고,
Landscape는 Landscape 모드에서 표시될 이미지이다.
Landscape를 따로 설정하지 않으면 Image에서 설정한 이미지를 그대로 사용한다.
Accessibillity는 저시력자를 위한 접근성 옵션 활성화 시 강조 효과에 사용할 이미지를 설정한다.
설정하지 않은다면 기본 이미지를 확대해서 표시한다.

Third 씬에는 이름을 Setting으로 변경하고, Image로 news-vector 이미지를 설정했다.
벡터 이미지는 보통 View에 적용될 때 높은 품질을 유지하며 Resizing 되지만,
Tab Bar Item에서는 예외이다.
따라서 권장 사이즈를 벗어나는 이미지를 사용하면 위와 같이 Tab Bar의 Frame을 벗어나는 문제가 발생한다.

이번엔 64pt의 Setting 이미지를 사용했다.

//
//  FirstSceneViewController.swift
//  ViewControllerPractice
//
//  Created by Martin.Q on 2021/10/29.
//

import UIKit

class FirstSceneViewController: UIViewController {
	
	@IBAction func SecondTabAction(_ sender: Any) {
		guard let secondChild = tabBarController?.children[1] else {
			return
		}
		tabBarController?.selectedViewController = secondChild
	}
	@IBAction func ThirdTabAction(_ sender: Any) {
		tabBarController?.selectedIndex = 2
	}
	
	override func awakeFromNib() {
		super.awakeFromNib()
		
		let regular = UIImage(named: "calendar_regular")
		let compact = UIImage(named: "calendar_compact")
		
		tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0)
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		tabBarItem.title = "First"
		tabBarItem.badgeValue = "Hot"
	}
}

First 씬의 awakeFromNib에서 Tab Bar Icon을 설정해 본다.
먼저 Calendar 이미지 인스턴스를 생성한다.

override func awakeFromNib() {
	super.awakeFromNib()
	let regular = UIImage(named: "calendar_regular")
	let compact = UIImage(named: "calendar_compact")
	
	let item = UITabBarItem(title: "Calendar", image: regular, selectedImage: compact)
	item.badgeColor = UIColor.black
	item.badgeValue = "7"
	
	tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0)
}

UITabBarItem(title:image:selectedImage:)메소드를 사용해 title과 각각의 이미지를 설정해 주고,
badeColor와 내용을 변경해 준다.

item.setBadgeTextAttributes(textAttributes: [String : Any]?, for: UIControl)

한 번 설정한 Badge text를 변경하기 위해선 setBadgeTextAttributes 메소드를 사용한다.

override func awakeFromNib() {
	super.awakeFromNib()
	let regular = UIImage(named: "calendar_regular")
	let compact = UIImage(named: "calendar_compact")
	
	let item = UITabBarItem(title: "Calendar", image: regular, selectedImage: compact)
	item.badgeColor = UIColor.black
	item.badgeValue = "7"
	
	tabBarItem = item
}

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

수정된 UITabBarItem을 tabBarItem에 저장한다.

대부분 변경사항이 잘 적용됐지만 몇 가지 문제가 있다.
Settings의 Icon이 너무 크고, Title을 침범했다.
Tab Bar Item의 아이콘은 크기에 맞게 자동으로 Resizing 되지만 권상 사이즈보다 큰 경우엔 그렇지 않다.
또한 Icon이 원본이 아닌 Template 방식으로 표시되고 있다.
Health 이미지는 의미가 변하지 않았지만 Canlendar 이미지는 내용이 사라져 의미를 알기 어렵다.
따라서 Tab Bar Item에 사용되는 이미지는 최대한 단순하고 직관적이어야 한다.
원본 그대로 표시하고 싶다면 ImageRendering 모드를 변경해야 한다.

asset의 Calendar 이미지를 선택하고,
Render As 옵션을 Original Image로 변경한다.

이번에 표시되는 Calendar의 Icon은 원본 이미지 그대로 표시된다.

Tab Bar Customizing

Tab Bar를 선택하고 Attribute Inspector를 확인해 보면
Customizing 할 수 있는 옵션들이 표시된다.

Background

해당 속성은 Tab Bar의 배경을 변경한다.

Shadow

해당 속성은 Tab Bar의 그림자를 설정한다.

두 이미지는 모두 화면의 크기에 맞게 크기가 조정될 수 있어야 한다.
Shadow 이미지는 Background와 함께 적용해야 정상적으로 표시된다.

Selection

선택 상태에서 표시할 이미지를 설정한다.
설정한 이미지는 왼쪽 사진과 같이 Item의 아래에 표시된다.

Image Tint

Tab Bar Item에 표시되는 이미지와 title은 기본 tint Color에 따른다.
해당 속성을 원하는 색으로 설정하면 title와 이미지에 모두 적용된다.

Style

Tab Bar는 두 가지 기본 Style을 제공한다.
Default는 흰색, Black은 검은색이다.
Translucent 설정을 통해 투명과 불투명을 설정할 수 있다.

Bar Tint

Background Color를 직접 설정할 때 사용한다.

Item Positioning

Tab Bar Item의 배치 방식을 결정한다.

Fill은 Item의 수만큼 너비를 균등하게 나누고 가운데 정렬로 배치한다.
Centered는 기본 여백과 높이로 배치한 다음 전체 아이템을 Tab Bar 중앙에 배치한다.

너비와 여백을 직접 조절할 때는 아래에 있는 Item Wiidth와 Item Spacing으로 조절한다.

Automatic으로 설정하게 되면
Size 클래스에 따라서 Fill과 Centered 중 하나를 자동으로 선택해 적용한다.
Horizontal Size가 Compact인 경우 Fill을, Regular인 경우 Centered를 선택하는 식이다.