Tự customize tab bar trong iOS

Đã bao giờ khi nhận một dự án iOS mới mà nhìn qua mockup và design spec thì bạn thấy những UI component cực kỳ dị, không theo các tiêu chuẩn guideline thông thường của Apple chưa? Ví dụ những yêu cầu oái oăm về tab bar của app chẳng hạn. Lúc này thì tab bar mặc định của iOS sẽ rất khó để customize và bạn phải tự tạo một tab bar cho riêng mình để có thể thỏa sức thay đổi mọi thứ.

UITabBarController mặc định của hệ thống rất ít tùy chọn để có thể customize. Mặc định thì thường có 5 item với cách bố trí icon, màu sắc selected, unselected như sau:

Chuyện gì xảy ra khi bạn bị yêu cầu phải làm một tab bar khác biệt như này:

Đầu tiên, mở Xcode lên và tạo project mới, mở file Main.storyboard và embed initial view controller vào một tab bar controller bằng menu Editor > Embed In > Tab Bar Controller.

Tiếp theo, chúng ta cần tạo một enum để chứa thông tin về các tab item trong tab bar. Tạo mới file TabItem.swift với đoạn code sau:

import UIKit

enum TabItem: String, CaseIterable {
    case calls = "calls"
    case photos = "photos"
    case contacts = "friends"
    case messages = "messages"

    var viewController: UIViewController {
        switch self {
        case .calls:
            return CallsViewController()
        case .contacts:
            return ContactsViewController()
        case .photos:
            return PhotosViewController()
        case .messages:
            return InboxViewController()
        }
    }

    var icon: UIImage {
        switch self {
        case .calls:
            return UIImage(named: "ic_phone")!
        case .photos:
            return UIImage(named: "ic_camera")!
        case .contacts:
            return UIImage(named: "ic_contacts")!
        case .messages:
            return UIImage(named: "ic_message")!
        }
    }
    
    var displayTitle: String {
        return self.rawValue.capitalized(with: nil)
    }
}

Đoạn enum trên khá đơn giản, nó chứa các thông tin cơ bản về các tab như: title, ảnh icon, view controller. Sau đó hãy tạo các view controller tương ứng cho mỗi tab tương ứng.

Tiếp theo, tạo mới file BaseTabBarController.swift, chứa class BaseTabBarController kế thừa class UITabBarController. Đây sẽ là class tab bar controller mặc định mới và có thể customize tùy ý.

Mở lại Main.storyboard và set class cho tab bar controller thành BaseTabBarController.

File BaseTabBarController.swift được implement như sau:

class BaseTabBarController: UITabBarController {
    var customTabBar: CustomTabBar!
    var tabBarHeight: CGFloat = 67.0

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

    func loadTabBar() {
        // Tạo và load custom tab bar
    }

    func setupCustomTabMenu(_ menuItems: [TabItem], completion: @escaping ([UIViewController]) -> Void) {
        // Handle custom tab bar và các attach touch event listener
    }

    func changeTab(tab: Int) {
        self.selectedIndex = tab
    }
}

Trong file này, chúng ta đã mở rộng height mặc định của tab bar controller lên một chút từ 48.0 lên 67.0.

Tiếp tục, hãy tạo file CustomTabBar.swift mới:

import UIKit

class CustomTabBar: UIView {
    var itemTapped: ((_ tab: Int) -> Void)?
    var activeItem: Int = 0

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    convenience init(menuItems: [TabItem], frame: CGRect) {
        self.init(frame: frame)
        // ...
    }

    func createTabItem(item: TabItem) -> UIView {
        // ...
        return UIView()
    }

    @objc func handleTap(_ sender: UIGestureRecognizer) {
        // ...
    }

    func switchTab(from: Int, to: Int) {
        // ...
    }

    func activateTab(tab: Int) {
        // ...
    }

    func deactivateTab(tab: Int) {
        // ...
    }
}

Class CustomTabBar này implement thành phần chủ đạo của tab bar controller của chúng ta. Trong method loadTabBar của class BaseTabBarController, thêm đoạn code sau:

    func loadTabBar() {
        let tabbarItems: [TabItem] = [.calls, .contacts, .photos, .messages]

        setupCustomTabMenu(tabbarItems, completion: { viewControllers in
            self.viewControllers = viewControllers
        })

        selectedIndex = 0 // Set default selected index thành item đầu tiên
    }

Tiếp tục implement method setupCustomTabMenu:

    func setupCustomTabMenu(_ menuItems: [TabItem], completion: @escaping ([UIViewController]) -> Void) {
        let frame = tabBar.frame
        var controllers = [UIViewController]()

        // Ẩn tab bar mặc định của hệ thống đi
        tabBar.isHidden = true
        // Khởi tạo custom tab bar
        customTabBar = CustomTabBar(menuItems: menuItems, frame: frame)
        customTabBar.translatesAutoresizingMaskIntoConstraints = false
        customTabBar.clipsToBounds = true
        customTabBar.itemTapped = changeTab(tab:)
        view.addSubview(customTabBar)
        view.backgroundColor = .white

        // Auto layout cho custom tab bar
        NSLayoutConstraint.activate([
            customTabBar.leadingAnchor.constraint(equalTo: tabBar.leadingAnchor),
            customTabBar.trailingAnchor.constraint(equalTo: tabBar.trailingAnchor),
            customTabBar.widthAnchor.constraint(equalToConstant: tabBar.frame.width),
            customTabBar.heightAnchor.constraint(equalToConstant: tabBarHeight),
            customTabBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])

        // Thêm các view controller tương ứng
        menuItems.forEach({
            controllers.append($0.viewController)
        })

        view.layoutIfNeeded()
        completion(controllers)
    }

Sau đó, implement convenience init method của CustomTabBar:

    convenience init(menuItems: [TabItem], frame: CGRect) {
        self.init(frame: frame)

        layer.backgroundColor = UIColor.white.cgColor

        // Khởi tạo từng tab bar item
        for index in 0 ..< menuItems.count {
            let itemWidth = frame.width / CGFloat(menuItems.count)
            let offsetX = itemWidth * CGFloat(index)

            let itemView = createTabItem(item: menuItems[index])
            itemView.translatesAutoresizingMaskIntoConstraints = false
            itemView.clipsToBounds = true
            itemView.tag = index

            addSubview(itemView)
            NSLayoutConstraint.activate([
                itemView.heightAnchor.constraint(equalTo: heightAnchor),
                itemView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: offsetX),
                itemView.topAnchor.constraint(equalTo: topAnchor),
            ])
        }

        setNeedsLayout()
        layoutIfNeeded()
        activateTab(tab: 0)
    }

Chỉnh sửa lại method createTabItem là chúng ta có thể tự do thoải mái customize từng tab bar item theo ý muốn.

    func createTabItem(item: TabItem) -> UIView {
        let tabBarItem = UIView()
        tabBarItem.layer.backgroundColor = UIColor.white.cgColor
        tabBarItem.translatesAutoresizingMaskIntoConstraints = false
        tabBarItem.clipsToBounds = true

        let itemTitleLabel = UILabel()
        itemTitleLabel.text = item.displayTitle
        itemTitleLabel.textAlignment = .center
        itemTitleLabel.translatesAutoresizingMaskIntoConstraints = false
        itemTitleLabel.clipsToBounds = true

        let itemImageView = UIImageView()
        itemImageView.image = item.icon.withRenderingMode(.automatic)
        itemImageView.translatesAutoresizingMaskIntoConstraints = false
        itemImageView.clipsToBounds = true

        tabBarItem.addSubview(itemTitleLabel)
        tabBarItem.addSubview(itemImageView)

        // Auto layout cho item title và item icon
        NSLayoutConstraint.activate([
            itemImageView.heightAnchor.constraint(equalToConstant: 25),
            itemImageView.widthAnchor.constraint(equalToConstant: 25),
            itemImageView.centerXAnchor.constraint(equalTo: tabBarItem.centerXAnchor),
            itemImageView.topAnchor.constraint(equalTo: tabBarItem.topAnchor, constant: 8),
            itemImageView.leadingAnchor.constraint(equalTo: tabBarItem.leadingAnchor, constant: 35),
            itemTitleLabel.heightAnchor.constraint(equalToConstant: 13),
            itemTitleLabel.widthAnchor.constraint(equalTo: tabBarItem.widthAnchor),
            itemTitleLabel.topAnchor.constraint(equalTo: itemImageView.bottomAnchor, constant: 4),
        ])

        // Thêm tap gesture recognizer để handle tap event
        tabBarItem.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))))

        return tabBarItem
    }

Cuối cùng, hoàn thiện các method xử lý khác như: handleTap, deactiveTab, switchTabactivateTab:

    @objc func handleTap(_ sender: UIGestureRecognizer) {
        switchTab(from: activeItem, to: sender.view!.tag)
    }

    func switchTab(from: Int, to: Int) {
        deactivateTab(tab: from)
        activateTab(tab: to)
    }

    func activateTab(tab: Int) {
        let tabToActivate = subviews[tab]
        let borderWidth = tabToActivate.frame.width - 20
        let borderLayer = CALayer()
        borderLayer.backgroundColor = UIColor.red.cgColor
        borderLayer.name = "Active Border"
        borderLayer.frame = CGRect(x: 10, y: 0, width: borderWidth, height: 2)

        DispatchQueue.main.async {
            UIView.animate(withDuration: 0.8,
                           delay: 0.0,
                           options: [.curveEaseIn, .allowUserInteraction]) {
                tabToActivate.layer.addSublayer(borderLayer)
                tabToActivate.setNeedsLayout()
                tabToActivate.layoutIfNeeded()
            } completion: { _ in
                self.itemTapped?(tab)
                self.activeItem = tab
            }
        }
    }

    func deactivateTab(tab: Int) {
        let inactiveTab = subviews[tab]
        let layerToRemove = inactiveTab.layer.sublayers?.filter({ $0.name == "Active Border" })

        DispatchQueue.main.async {
            UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseIn, .allowUserInteraction]) {
                layerToRemove?.forEach({ $0.removeFromSuperlayer() })
                inactiveTab.setNeedsLayout()
                inactiveTab.layoutIfNeeded()
            } completion: { _ in

            }
        }
    }

Run app và chúng ta sẽ có một tab bar mới với hiệu ứng chuyển tab được customize như sau:

Source article: https://medium.com/sprinthub/creating-a-customized-tab-bar-in-ios-with-swift-41ed380f2a30

Complete source: https://github.com/oNguyenXuanThanh/StudyReport112020


All Rights Reserved