관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Custom TabBar 구현 방법 (커스텀 탭바, UIStackView, fillEqually) 본문

UI 컴포넌트 (swift)

[iOS - swift] Custom TabBar 구현 방법 (커스텀 탭바, UIStackView, fillEqually)

jake-kim 2023. 1. 8. 23:22

구현된 커스텀 탭바

Custom TabBar 구현 아이디어

  • StackView의 distribution을 fillEqually로, alignment를 center로 설정하고 각 버튼을 삽입하여 구현
    • distribution을 filleEqually로 설정하면 스택뷰안의 아이템들이 동일한 크기로 배치

Custom TabBar 구현

  • 구현에 사용된 extension
    • UIControl에 추가한 addAction은 버튼의 이벤트 처리를 할  때, addTarget 방식이 아닌 클로저 처리 방식으로 사용
    • UIImage에 추가한 alpha 메소드는 이미지에 alpha값을 부여하는 메소드
// https://ios-development.tistory.com/1237
public extension UIControl {
    func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping () -> ()) {
        @objc class ClosureSleeve: NSObject {
            let closure: () -> ()
            
            init(_ closure: @escaping () -> ()) {
                self.closure = closure
            }
            
            @objc func invoke() {
                closure()
            }
        }
        
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, "\(UUID())", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

// https://stackoverflow.com/questions/28517866/how-to-set-the-alpha-of-an-uiimage-in-swift-programmatically
extension UIImage {
    func alpha(_ value:CGFloat) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        draw(at: CGPoint.zero, blendMode: .normal, alpha: value)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage!
    }
}
  • 탭바에 들어가는 항목들을 enum으로 정의하고, normalImage와 selectedImage를 따로 정의
    • normalImage: 선택되지 않은 탭
    • selectedImage: 선택된 탭
enum TabItem: Int {
    case home
    case chat
    case my
    
    var normalImage: UIImage? {
        switch self {
        case .home:
            return UIImage(systemName: "house")
        case .chat:
            return UIImage(systemName: "message")
        case .my:
            return UIImage(systemName: "person.crop.circle")
        }
    }
    
    var selectedImage: UIImage? {
        switch self {
        case .home:
            return UIImage(systemName: "house.fill")
        case .chat:
            return UIImage(systemName: "message.fill")
        case .my:
            return UIImage(systemName: "person.crop.circle.fill")
        }
    }
}
  • class CustomTabBar 선언
final class CustomTabBar: UIView {

}
  • stackView와 필요한 프로퍼티 정의
    • tabButtons을 전역으로 빼놓는 이유는, updateUI에서 선택된 인덱스값을 바탕으로 setImage해주기 위함
    private let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.alignment = .center
        return stackView
    }()
    
    private let tabItems: [TabItem]
    private var tabButtons = [UIButton]()
    private var selectedIndex = 0 {
        didSet { updateUI() }
    }
    
    private func updateUI() {
        tabItems
            .enumerated()
            .forEach { i, item in
                let isButtonSelected = selectedIndex == i
                let image = isButtonSelected ? item.selectedImage : item.normalImage
                let selectedButton = tabButtons[i]
                
                selectedButton.setImage(image, for: .normal)
                selectedButton.setImage(image?.alpha(0.5), for: .highlighted)
        }
    }
  • 초기화 부분
    • tamItems만큼 버튼을 만들고 레이아웃 설정
    • 오토레이아웃 설정은 stackView만 하면 되므로, 따로 버튼에 관해 레이아웃 설정을 하지 않아도 되므로 편리 (stackView의 fillEqually가 적용)
    init(tabItems: [TabItem]) {
        self.tabItems = tabItems
        super.init(frame: .zero)
        setUp()
    }
    required init?(coder: NSCoder) {
        fatalError()
    }
    
    private func setUp() {
        defer { updateUI() }
        
        tabItems
            .enumerated()
            .forEach { i, item in
                let button = UIButton()
                button.setImage(item.normalImage, for: .normal)
                button.setImage(item.normalImage?.alpha(0.5), for: .highlighted)
                button.addAction { [weak self] in
                    self?.selectedIndex = i
                }
                tabButtons.append(button)
                stackView.addArrangedSubview(button)
            }
        
        backgroundColor = .systemGray.withAlphaComponent(0.2)
        
        addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            stackView.leftAnchor.constraint(equalTo: leftAnchor),
            stackView.rightAnchor.constraint(equalTo: rightAnchor),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
            stackView.topAnchor.constraint(equalTo: topAnchor),
        ])
    }

사용하는 쪽

  • CustomTabBar 인스턴스를 만듣 후 오토 레이아웃을 설정하여 사용
class ViewController: UIViewController {
    let tabBarView = CustomTabBar(tabItems: [.home, .chat, .my])
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(tabBarView)
        
        tabBarView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            tabBarView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
            tabBarView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
            tabBarView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            tabBarView.heightAnchor.constraint(equalToConstant: 56),
            tabBarView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),
        ])
        
    }
}

* 전체 코드: https://github.com/JK0369/ExCustomTabBar

Comments