티스토리 뷰
기존 프로젝트를 스토리보드에서 코드기반 UI로 리팩토링하면서 네비게이션이 복잡해져 코디네이터 패턴을 적용하고 있는데요,
가장 기본이 되는 TabBar를 코드로 작성하는 방법을 공유해보려고 합니다!
탭바가 생각보다 복잡한 구조를 가지고 있어서 설정하기가 좀 어려웠습니다. ʕᴗ̩̩ᴥᴗ̩̩ʔ
그래서 코디네이터 패턴보단 탭바의 구조부터 자세히 알아보고 실제 코드에서 코디네이터패턴을 적용해 보여드리겠습니다.
TabBarController 구성 알아보기
TabBar를 생성하기 위해 TabBar가 어떻게 구성되는지 알아보겠습니다.
TabBar는 UITabBarController로 생성할 수 있는데요,
이 UITabBarController안에는 Bar자체인 UITabBar가 있고 그 안에 홈, 설정 등 각 뷰를 담당하는 UITabItem들이 있습니다.
그리고 매우 중요한 ⭐️UIViewControllers⭐️가 있습니다!
사실상 UITabBar는 TabBar의 스타일 지정을 할 때만 쓰이고 대부분의 기능들은 UIViewControllers를 통해 설정됩니다.
viewControllers속성은 [UIViewController]형태로 여러개의 ViewController를 할당받을 수 있습니다.
이 개수에 의해서 TabBar의 개수가 결정되고 이 ViewController에 의해서 각 탭에 따른 뷰가 동작합니다.
그리고 이 UIViewControllers는 UINavigationController도 할당받을 수 있는데요
탭 바 내부에서 화면 전환이 이뤄지는 경우엔 단일 뷰가 아니어서 Navigation이 필요해지므로 이 때 UINavigationController를 할당해주면 됩니다.
즉 이런 형태가 됩니다.
RootView(==Window) 위에 탭바뷰가 올라가고 그 위에 네비게이션뷰가 올라가서 실제 뷰를 띄워주고 탭 내에서 화면전환을 해주는 것입니다. (탭 간의 전환은 탭뷰가 담당)
TabBarController안에 NavigationController는 기본적으로는 상단의 네비게이션뷰를 컨트롤하는 속성들을 가지고 있습니다.
그러나 그 외에 tabBarItem이라는 속성도 있는데요, 이 속성에 UITabBarItem을 할당하면
UINavigationController가 UITabBarController에 추가될 때, TabBarItem과 해당 컨트롤러를 매칭하여줍니다.
NavigationViewController와 TabBarItem 설정법
먼저 UITabBarController는 UIVIewControllers를 가지고 있다고 하였는데,
이 속성은 setViewControllers 속성을 통해 설정 가능합니다.
self.tabBarController.setViewControllers(tabNavigationControllers, animated: false)
또한, TabBarItem을 설정하려면
tabNavigationController.tabBarItem = UITabBarItem(
title: page.toKrName(),
image: UIImage(systemName: page.toIconName()),
tag: page.toInt()
)
이런식으로 UINavigationController의 tabBarItem속성을 통해 아이템을 지정해주면됩니다.
tabBarItems는 UITabBarController > tabBar > items로도 접근할 수 있지만
UITabBarController > viewController(UINavigationController) > tabBarItem를 통해 설정하면 NavigationController가 TabBarController에 추가될 때 매칭되어 설정됩니다.
설계하기
위 내용들을 기반으로 TabBar를 만들기 위한 코드를 설계 해보겠습니다.
먼저 필요한 설정들을 생각해보면 다음과 같습니다.
1. UITabBarController 생성하기
2. 탭 별 TabBarItem 생성하기
3. 각 탭이 동작하도록 NavigationController 생성하고 TabBarItem 연결하기
4. 각 탭에 맞는 뷰의 Coordinator 실행하고 각 NavigationController에 추가하기
5. UITabBarController에 NavigationController 설정하기
6. root view에 tabBar 추가하기
이를 각 메소드로 바꿔보겠습니다.
1. UITabBarController 생성하기 -> 클래스변수로 만들고 init에서 생성
2. 탭 별 TabBarItem 생성하기 -> createTabBarItem
3. 각 탭이 동작하도록 NavigationController 생성하고 TabBarItem 연결하기 -> createNavigationController
4. 각 탭에 맞는 뷰의 Coordinator 실행하고 각 NavigationController에 추가하기 -> startTabCoordinator
5. UITabBarController에 NavigationController 설정하기 -> configureTabBarController
6. root view에 tabBarController 추가하기 -> addTabBarController
메소드로 변환했으니 필요한 파라미터와 retrun 값도 정해보겠습니다.
2. TabBarItem 생성을 위해 TabBarItem 정보 받기
func createTabBarItem(tabBarItemType: TabBarItemType) -> UITabBarITem
*TabBarItem 정보를 담을 enum을 만들 예정
3. 탭 바 별로 NavigationController를 생성하고 TabBarItem을 지정해주기
func createNavigationController(tabBarItems: [UITabBarItem]) -> UINavigationController
4. 탭 별 NavigationController위에 코디네이터를 실행하기 위해 NavigationController받기
func startTabCoordinator(tabNavigationController: UINavigationController)
5. TabBarController와 NavigationController연결을 위해 NavigationController 받기
func configureTabBarController(tabNavigationControllers: [UINavigationController])
6. tabBarController와 root 네비게이션은 클래스멤버로 있으니 파라미터와 리턴 X
func addTabBarController()
Protocol 작성
일단 기본적인 Coordinator Protocol과 이를 상속하는 TabBarCoordinator 프로토콜을 만들어줍니다.
*위에서 작성한 메서드들은 외부에서 접근할 일이 없으므로 private func으로 선언하기 위해 프로토콜에 적어주지 않겠습니다.
// Coordinator.swift
import UIKit
protocol Coordinator: AnyObject {
var finishDelegate: CoordinatorFinishDelegate? { get set }
var navigationController: UINavigationController { get set }
var childCoordinators: [Coordinator] { get set }
var type: CoordinatorType { get }
init(_ navigationController: UINavigationController)
func start()
func finish()
func findCoordinator(type: CoordinatorType) -> Coordinator?
}
extension Coordinator {
func finish() {
childCoordinators.removeAll()
finishDelegate?.coordinatorDidFinish(childCoordinator: self)
}
func findCoordinator(type: CoordinatorType) -> Coordinator? {
var stack: [Coordinator] = [self]
while !stack.isEmpty {
let currentCoordinator = stack.removeLast()
if currentCoordinator.type == type {
return currentCoordinator
}
currentCoordinator.childCoordinators.forEach({ child in
stack.append(child)
})
}
return nil
}
}
// TabBarCoordinator.swift
import UIKit
protocol TabBarCoordinator: Coordinator {
var tabBarController: UITabBarController { get set }
}
Class 구현
1. 구현에 앞서 TabBarItem을 구분할 enum을 만들어주겠습니다.
// TabBarItemType.swift
enum TabBarItemType: String, CaseIterable {
case home, scrap, info
// Int형에 맞춰 초기화
init?(index: Int) {
switch index {
case 0: self = .home
case 1: self = .scrap
case 2: self = .info
default: return nil
}
}
/// TabBarPage 형을 매칭되는 Int형으로 반환
func toInt() -> Int {
switch self {
case .home: return 0
case .scrap: return 1
case .info: return 2
}
}
/// TabBarPage 형을 매칭되는 한글명으로 변환
func toKrName() -> String {
switch self {
case .home: return "홈"
case .scrap: return "스크랩"
case .info: return "내정보"
}
}
/// TabBarPage 형을 매칭되는 아이콘명으로 변환
func toIconName() -> String {
switch self {
case .home: return "house"
case .scrap: return "star"
case .info: return "person"
}
}
}
필요한 탭바에 맞춰 구현해주시면 됩니다.
여기서는 home, scrap, info 3개의 탭을 구성했습니다.
부가적인 기능을 할 함수들도 추가로 구현해주었습니다.
2. 이제 구현을 위해 TabBarCoordinator 프로토콜을 상속하는 클래스를 만들어줍니다.
// DefaultTabBarCoordinator.swift
class DefaultTabBarCoordinator: TabBarCoordinator{
var finishDelegate: CoordinatorFinishDelegate?
var navigationController: UINavigationController
var childCoordinators: [Coordinator]
var type: CoordinatorType
var tabBarController: UITabBarController
required init(_ navigationController: UINavigationController) {
}
func start() {
}
}
프로토콜을 따르도록 Fix해주면 위와 같은 형태가 됩니다.
3. 위에서 설계한 TabBarController를 만들기 위한 메소드를 추가해줍니다.
class DefaultTabBarCoordinator: TabBarCoordinator{
var finishDelegate: CoordinatorFinishDelegate?
var navigationController: UINavigationController
var childCoordinators: [Coordinator]
var type: CoordinatorType
var tabBarController: UITabBarController
required init(_ navigationController: UINavigationController) {
}
func start() {
}
// MARK: - TabBarController 설정 메소드
private func configureTabBarController(tabNavigationControllers: [UIViewController]){
}
private func addTabBarController(){
}
private func createTabBarItem(of page: TabBarItemType) -> UITabBarItem {
}
private func createTabNavigationController(tabBarItem: UITabBarItem) -> UINavigationController{
}
private func startTabCoordinator(tabNavigationController: UINavigationController) {
}
}
4. init을 구현해줍니다.
required init(_ navigationController: UINavigationController) {
self.navigationController = navigationController
self.type = CoordinatorType.tab
// 탭바 생성
self.tabBarController = UITabBarController()
}
NavigationController를 주입받아 설정해주고 tabBarController의 인스턴스를 생성해줍니다.
⬇️ CoordinatorType?
CoordinatorType은 각 Coordinator를 구분하기 위한 enum이고 구현은 다음과 같습니다.
enum CoordinatorType {
case home, scrap, info
case tab
}
5. TabBarController 설정 메소드들을 구현해줍니다.
/// 탭바 아이템 생성
private func createTabBarItem(of page: TabBarItemType) -> UITabBarItem {
return UITabBarItem(
title: page.toKrName(),
image: UIImage(systemName: page.toIconName()),
tag: page.toInt()
)
}
TabBarItemType을 받아 UITabBarItem으로 변환해주는 함수입니다.
/// 탭바 페이지대로 탭바 생성
private func createTabNavigationController(tabBarItem: UITabBarItem) -> UINavigationController {
let tabNavigationController = UINavigationController()
// 상단에서 NavigationBar 숨김 해제
tabNavigationController.setNavigationBarHidden(false, animated: false)
// 상단 NavigationBar에 title 설정
tabNavigationController.navigationBar.topItem?.title = TabBarItemType(index: tabBarItem.tag)?.toKrName()
// tabBarItem 설정을 통해 NavigationController와 tabBarItem를 연결
tabNavigationController.tabBarItem = tabBarItem
return tabNavigationController
}
각 TabBarItem에 맞는 NavigationController을 생성하고 설정해줍니다.
private func startTabCoordinator(tabNavigationController: UINavigationController) {
// tag 번호로 TabBarPage로 변경
let tabBarItemTag: Int = tabNavigationController.tabBarItem.tag
guard let tabBarItemType: TabBarItemType = TabBarItemType(index: tabBarItemTag) else { return }
// 코디네이터 생성 및 실행
switch tabBarItemType {
case .home:
let homeCoordinator = DefaultHomeCoordinator(tabNavigationController)
homeCoordinator.finishDelegate = self
self.childCoordinators.append(homeCoordinator)
homeCoordinator.start()
case .scrap:
let scrapCoordinator = DefaultScrapCoordinator(tabNavigationController)
scrapCoordinator.finishDelegate = self
self.childCoordinators.append(scrapCoordinator)
scrapCoordinator.start()
case .info:
let infoCoordinator = DefaultInfoCoordinator(tabNavigationController)
infoCoordinator.finishDelegate = self
self.childCoordinators.append(infoCoordinator)
infoCoordinator.start()
}
}
각 tabBarNavigationController에 맞는 코디네이터를 실행해줍니다.
⬇️ 기본 코디네이터 작성 형태
// HomeCoordinator.swift
protocol HomeCoordinator: Coordinator {
var homeViewController: HomeViewController { get set }
}
// DefaultHomeCoordinator.swift
import UIKit
class DefaultHomeCoordinator: HomeCoordinator {
weak var finishDelegate: CoordinatorFinishDelegate?
var navigationController: UINavigationController
var homeViewController: HomeViewController
var childCoordinators: [Coordinator] = []
var type: CoordinatorType = .home
required init(_ navigationController: UINavigationController) {
self.navigationController = navigationController
self.homeViewController = HomeViewController()
}
func start() {
self.navigationController.pushViewController(self.homeViewController, animated: true)
}
}
extension DefaultHomeCoordinator: CoordinatorFinishDelegate {
func coordinatorDidFinish(childCoordinator: Coordinator) {
// 자식 뷰를 삭제하는 델리게이트 (자식 -> 부모 접근 -> 부모에서 자식 삭제)
self.childCoordinators = self.childCoordinators
.filter({ $0.type != childCoordinator.type })
childCoordinator.navigationController.popToRootViewController(animated: true)
}
}
/// 탭바 스타일 지정 및 초기화
private func configureTabBarController(tabNavigationControllers: [UIViewController]) {
// TabBar의 VC 지정
self.tabBarController.setViewControllers(tabNavigationControllers, animated: false)
// home의 index로 TabBar Index 세팅
self.tabBarController.selectedIndex = TabBarItemType.home.toInt()
// TabBar 스타일 지정
self.tabBarController.view.backgroundColor = .systemBackground
self.tabBarController.tabBar.backgroundColor = .systemBackground
self.tabBarController.tabBar.tintColor = UIColor.black
}
TabBarController에 NavigationController들을 추가해주고 탭바 스타일을 설정해줍니다.
private func addTabBarController(){
// 화면에 추가
self.navigationController.pushViewController(self.tabBarController, animated: true)
}
루트 뷰에 탭바를 추가해줍니다.
6. 5번의 함수들을 조합하여 실행시켜주는 start()함수를 작성해줍니다.
자세한 사항은 주석으로 작성하였습니다.
func start() {
// 1. 탭바 아이템 리스트 생성
let pages: [TabBarItemType] = TabBarItemType.allCases
// 2. 탭바 아이템 생성
let tabBarItems: [UITabBarItem] = pages.map { self.createTabBarItem(of: $0) }
// 3. 탭바별 navigation controller 생성
let controllers: [UINavigationController] = tabBarItems.map {
self.createTabNavigationController(tabBarItem: $0)
}
// 4. 탭바별로 코디네이터 생성하기
let _ = controllers.map{ self.startTabCoordinator(tabNavigationController: $0) }
// 5. 탭바 스타일 지정 및 VC 연결
self.configureTabBarController(tabNavigationControllers: controllers)
// 6. 탭바 화면에 붙이기
self.addTabBarController()
}
7. 마지막으로 Coordinator 종료 시 실행할 Delegate를 구현해줍니다.
extension DefaultTabBarCoordinator: CoordinatorFinishDelegate {
func coordinatorDidFinish(childCoordinator: Coordinator) {
self.childCoordinators.removeAll()
self.navigationController.viewControllers.removeAll()
self.finishDelegate?.coordinatorDidFinish(childCoordinator: self)
}
}
일반적으로 FinishiDelegate는 메모리해제되도록 부모 코디네이터의 childCoordinator에서 본인을 지우고 rootview에서 pop하는 코드로 구성되지만,
TabBar의 경우 TabBar가 종료된다는건 앱의 종료를 의미하므로 navigationController에서 모든 VC를 지우고 상위뷰에서도 본인을 지워줍니다.
이렇게 TabBarCoordinator 구현은 마무리되었습니다.
TabBarCoordinator보다 상위에서 동작하는 AppCoordinator에서 TabBarCoordinator를 실행해주면
이렇게 탭바가 잘 들어가는 모습을 볼 수 있습니다.
⬇️ 전체코드
// TabBarItemType.swift
enum TabBarItemType: String, CaseIterable {
case home, scrap, info
// Int형에 맞춰 초기화
init?(index: Int) {
switch index {
case 0: self = .home
case 1: self = .scrap
case 2: self = .info
default: return nil
}
}
/// TabBarPage 형을 매칭되는 Int형으로 반환
func toInt() -> Int {
switch self {
case .home: return 0
case .scrap: return 1
case .info: return 2
}
}
/// TabBarPage 형을 매칭되는 한글명으로 변환
func toKrName() -> String {
switch self {
case .home: return "홈"
case .scrap: return "스크랩"
case .info: return "내정보"
}
}
/// TabBarPage 형을 매칭되는 아이콘명으로 변환
func toIconName() -> String {
switch self {
case .home: return "house"
case .scrap: return "star"
case .info: return "person"
}
}
}
// Coordinator.swift
import UIKit
protocol Coordinator: AnyObject {
var finishDelegate: CoordinatorFinishDelegate? { get set }
var navigationController: UINavigationController { get set }
var childCoordinators: [Coordinator] { get set }
var type: CoordinatorType { get }
init(_ navigationController: UINavigationController)
func start()
func finish()
func findCoordinator(type: CoordinatorType) -> Coordinator?
}
extension Coordinator {
func finish() {
childCoordinators.removeAll()
finishDelegate?.coordinatorDidFinish(childCoordinator: self)
}
func findCoordinator(type: CoordinatorType) -> Coordinator? {
var stack: [Coordinator] = [self]
while !stack.isEmpty {
let currentCoordinator = stack.removeLast()
if currentCoordinator.type == type {
return currentCoordinator
}
currentCoordinator.childCoordinators.forEach({ child in
stack.append(child)
})
}
return nil
}
}
// TabBarCoordinator.swift
import UIKit
protocol TabBarCoordinator: Coordinator {
var tabBarController: UITabBarController { get set }
}
// DefaultTabBarCoordinator.swift
import Foundation
import UIKit
class DefaultTabBarCoordinator: TabBarCoordinator {
var finishDelegate: CoordinatorFinishDelegate?
var tabBarController: UITabBarController
var navigationController: UINavigationController
var childCoordinators: [Coordinator] = []
var type: CoordinatorType
required init(_ navigationController: UINavigationController) {
self.navigationController = navigationController
self.type = CoordinatorType.tab
// 탭바 생성
self.tabBarController = UITabBarController()
}
/// 탭바 설정 함수들의 흐름 조정
func start() {
// 1. 탭바 아이템 리스트 생성
let pages: [TabBarItemType] = TabBarItemType.allCases
// 2. 탭바 아이템 생성
let tabBarItems: [UITabBarItem] = pages.map { self.createTabBarItem(of: $0) }
// 3. 탭바별 navigation controller 생성
let controllers: [UINavigationController] = tabBarItems.map {
self.createTabNavigationController(tabBarItem: $0)
}
// 4. 탭바별로 코디네이터 생성하기
let _ = controllers.map{ self.startTabCoordinator(tabNavigationController: $0) }
// 5. 탭바 스타일 지정 및 VC 연결
self.configureTabBarController(tabNavigationControllers: controllers)
// 6. 탭바 화면에 붙이기
self.addTabBarController()
}
// MARK: - TabBarController 설정 메소드
/// 탭바 스타일 지정 및 초기화
private func configureTabBarController(tabNavigationControllers: [UIViewController]) {
// TabBar의 VC 지정
self.tabBarController.setViewControllers(tabNavigationControllers, animated: false)
// home의 index로 TabBar Index 세팅
self.tabBarController.selectedIndex = TabBarItemType.home.toInt()
// TabBar 스타일 지정
self.tabBarController.view.backgroundColor = .systemBackground
self.tabBarController.tabBar.backgroundColor = .systemBackground
self.tabBarController.tabBar.tintColor = UIColor.black
}
private func addTabBarController(){
// 화면에 추가
self.navigationController.pushViewController(self.tabBarController, animated: true)
}
/// 탭바 아이템 생성
private func createTabBarItem(of page: TabBarItemType) -> UITabBarItem {
return UITabBarItem(
title: page.toKrName(),
image: UIImage(systemName: page.toIconName()),
tag: page.toInt()
)
}
/// 탭바 페이지대로 탭바 생성
private func createTabNavigationController(tabBarItem: UITabBarItem) -> UINavigationController {
let tabNavigationController = UINavigationController()
tabNavigationController.setNavigationBarHidden(false, animated: false)
tabNavigationController.navigationBar.topItem?.title = TabBarItemType(index: tabBarItem.tag)?.toKrName()
tabNavigationController.tabBarItem = tabBarItem
return tabNavigationController
}
private func startTabCoordinator(tabNavigationController: UINavigationController) {
// tag 번호로 TabBarPage로 변경
let tabBarItemTag: Int = tabNavigationController.tabBarItem.tag
guard let tabBarItemType: TabBarItemType = TabBarItemType(index: tabBarItemTag) else { return }
// 코디네이터 생성 및 실행
switch tabBarItemType {
case .home:
let homeCoordinator = DefaultHomeCoordinator(tabNavigationController)
homeCoordinator.finishDelegate = self
self.childCoordinators.append(homeCoordinator)
homeCoordinator.start()
case .scrap:
let scrapCoordinator = DefaultScrapCoordinator(tabNavigationController)
scrapCoordinator.finishDelegate = self
self.childCoordinators.append(scrapCoordinator)
scrapCoordinator.start()
case .info:
let infoCoordinator = DefaultInfoCoordinator(tabNavigationController)
infoCoordinator.finishDelegate = self
self.childCoordinators.append(infoCoordinator)
infoCoordinator.start()
}
}
}
extension DefaultTabBarCoordinator: CoordinatorFinishDelegate {
func coordinatorDidFinish(childCoordinator: Coordinator) {
self.childCoordinators.removeAll()
self.navigationController.viewControllers.removeAll()
self.finishDelegate?.coordinatorDidFinish(childCoordinator: self)
}
}
마치며
뭔가 장황하게 썼다 싶기도 하지만 구현했던 흐름을 따라가며 작성해보았습니다...
사실 AppCoordinator에 대한 부분이 우선되어야하긴 하지만 TabBar가 너무 복잡해서 먼저 작성하였습니다.
추후에 작성해보도록 하겠습니다.
도움이 되었으면 좋겠습니다.
감사합니다.
Ref.
[iOS] - UITabBarController 코드로 작성하기
boostcampwm-2021/iOS06-MateRunner
'iOS > UIKit' 카테고리의 다른 글
[iOS] PageViewController 직접 구현하기 + UIPageViewController 오류 (2) | 2024.07.13 |
---|---|
[iOS] Tuist 모듈화 후 CollectionView Cell 초기화 오류 (1) | 2024.03.25 |
- Total
- Today
- Yesterday
- Swift Concurrency
- TestCode
- SwiftUI
- RX
- MVC
- Bloking/Non-bloking
- combine
- healthkit
- DocC
- 비동기/동기
- GetX
- SWM
- notion
- swift
- 코디네이터 패턴
- MVVM
- coordinator pattern
- design pattern
- 리액티브 프로그래밍
- Architecture Pattern
- MVI
- Flutter
- 프로그래머스
- 노션
- reactive programming
- ios
- 소프트웨어마에스트로
- Flux
- programmers
- 아키텍쳐 패턴
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |