[iOS/Uikit] Custom TabBarController 구현

전성훈·2023년 11월 1일
2

iOS/UIKit

목록 보기
1/3
post-thumbnail

주제: 커스텀 탭 바 컨트롤러 구현하기

customTabBarController


  • 앱 개발 과정에서 때때로 TabBarController의 탭 바 스타일을 커스터마이즈할 필요가 있습니다. 그러나 Apple이 제공하는 기본 TabBarController에서는 스타일 변경이 제한적입니다.
  • 이를 해결하기 위해 CustomTabBarController를 구현하면, 보다 유연한 탭 바 스타일링과 관리가 가능해집니다.

구현 방법

  1. 클래스 정의 및 변수 초기화
    • CustomTabBarControllerUIViewController를 상속받아 정의되어 있습니다.
    • viewControllers 배열은 각 탭에 대응하는 뷰 컨트롤러를 저장합니다.
    • buttons 배열은 탭 바에 표시될 버튼들을 저장합니다.
    • tabBarView는 탭 바를 표시하는 뷰 입니다.
  2. 탭 바 설정
    • setupTabBar 함수는 tabBarView를 화면 하단에 위치시키고, 적절한 높이와 폭으로 설정합니다.
  3. 버튼 설정
    • setupButtons 함수는 각 viewControllers에 대해 버튼을 생성하고, 이를 tabBarView에 추가합니다.
    • 각 버튼의 태그는 해당 뷰 컨트롤러의 인덱스로 설정됩니다.
    • 버튼이 탭되면 tabButtonTapped 함수가 호출되어, selectedInedx가 갱신됩니다.
  4. 뷰 업데이트
    • updateView 함수는 현재 선택된 인덱스에 따라 해당 뷰 컨트롤러를 활성화하고, 다른 모든 뷰 컨트롤러를 비활성화합니다.
    • 선택된 뷰 컨트롤러의 뷰는 tabBarView 아래에 위치합니다.
  5. 선택한 탭 업데이트
    • selectedInedx의 값이 변경되면, updateView 함수가 호출되어 뷰가 갱신됩니다.

코드

CustomTabBarController

import UIKit

final class CustomTabBarController: UIViewController {
    
    private lazy var viewControllers: [UIViewController] = []
    private lazy var buttons: [UIButton] = []
    
    private lazy var tabBarView: UIView = {
        let view = UIView()
        
        view.backgroundColor = .white
        view.layer.cornerRadius = 35
        
        return view
    }()
    
    var selectedIndex = 0 {
	    willSet { 
		    previewsIndex = selectedIndex
	    }
        didSet {
            updateView()
        }
    }
	private var previewsIndex = 0 
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTabBar()
    }
    
    func setViewControllers(_ viewControllers: [UIViewController]) {
        self.viewControllers = viewControllers
        setupButtons()
        updateView()
    }
    
    private func setupTabBar() {
        view.addSubview(tabBarView)
        
        tabBarView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tabBarView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tabBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tabBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tabBarView.heightAnchor.constraint(equalToConstant: 90)
        ])
    }
    
    private func setupButtons() {
	    // 버튼의 넓이는 tab 개수에 맞춰서 유동적으로 변함 
        let buttonWidth = view.bounds.width / CGFloat(viewControllers.count)
        
        for (index, viewController) in viewControllers.enumerated() {
            let button = UIButton()
            button.tag = index
            button.setTitle(viewController.title, for: .normal)
            button.setTitleColor(.black, for: .normal)
            button.addTarget(self, action: #selector(tabButtonTapped(_:)), for: .touchUpInside)
            tabBarView.addSubview(button)
	        
            button.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                button.bottomAnchor.constraint(equalTo: tabBarView.bottomAnchor),
                button.leadingAnchor.constraint(equalTo: tabBarView.leadingAnchor, constant: CGFloat(index) * buttonWidth),
                button.widthAnchor.constraint(equalToConstant: buttonWidth),
                button.heightAnchor.constraint(equalTo: tabBarView.heightAnchor)
            ])
            
            buttons.append(button)
        }
    }
    private func updateView() {
        deleteView()
        setupView()
		
        buttons.forEach { $0.isSelected = ($0.tag == selectedIndex) }
    }
	    
    private func deleteView() {
        let previousVC = viewControllers[previewsIndex]
        previousVC.willMove(toParent: nil)
        previousVC.view.removeFromSuperview()
        previousVC.removeFromParent()
    }
	    
    private func setupView() {
        let selectedVC = viewControllers[selectedIndex]
        
        self.addChild(selectedVC)
        view.insertSubview(selectedVC.view, belowSubview: tabBarView)
        selectedVC.view.frame = view.bounds
        selectedVC.didMove(toParent: self)
    }
    
    @objc private func tabButtonTapped(_ sender: UIButton) {
        selectedIndex = sender.tag
    }
}
  • 위 TabBarController를 SceneDeleagate에서 생성해주면 됩니다.
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        
        let firstViewController = TestViewController("1")
        firstViewController.view.backgroundColor = .gray
        firstViewController.title = "First"
        let firstNavi = UINavigationController(rootViewController: firstViewController)
        
        let secondViewController = TestViewController("2")
        secondViewController.view.backgroundColor = .darkGray
        secondViewController.title = "Second"
        let secondNavi = UINavigationController(rootViewController: secondViewController)
        
        let thirdViewController = TestViewController("3")
        thirdViewController.view.backgroundColor = .lightGray
        thirdViewController.title = "Third"
        let thirdNavi = UINavigationController(rootViewController: thirdViewController)
        
        let customTabBarController = CustomTabBarController()
        customTabBarController.setViewControllers([firstNavi, secondNavi, thirdNavi])
        
        window?.rootViewController = customTabBarController
        window?.makeKeyAndVisible()
    }
}
  • TabBarViewController 내부에 생성된 세개의 ViewController의 life cycle 확인을 위해 커스텀해준 ViewController를 반영합니다.
import UIKit

final class TestViewController: UIViewController {
    var number = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        print("\(number): ViewDidLoad")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        print("\(number): ViewWillAppear")
    }
    
    init(_ number: String) {
        super.init(nibName: nil, bundle: nil)
        
        self.number = number
        
        print("\(number): init")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        print("\(number): Deinit")
    }
}
  • 최초 실행 때
1: init
1: ViewDidLoad
2: init
2: ViewDidLoad
3: init
3: ViewDidLoad
1: ViewWillAppear
  • TabBar Button을 클릭 할 때
2: ViewWillAppear
3: ViewWillAppear
1: ViewWillAppear

TabBar Button을 클릭 할 때를 보면 ViewWillAppear만 호출되는 것을 확인할 수 있다.

Tab Bar Delegate

  • CustomTabBarController 내부에 존재하는 UINavigationController 에서 TabBarhidden 처리 한다던가 색상을 변경하는 등 TabBar의 속성을 변경하려고 하면 Delegate를 생성해서 해당 ViewController나 flow를 담당하는 부분에서 채택하면 된다.
protocol TabBarDelegate: AnyObject { 
	func shouldHideTabBar(_ hide: Bool)
}

final class CustomTabBarController: UIViewController, TabBarDelegate { 
	func shouldHideTabBar(_ hide: Bool) { 
		tabBarView.isHidden = hide
	}
}

final class TextViewController: UIViewController { 
	weak var tabBarViewController: TabBarDelegate?
}

출처(참고문헌)

원본 코드

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글