// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit public final class TabBar: UIView { private let tabs: [Tab] private var accentLineViewHorizontalCenteringConstraint: NSLayoutConstraint! private var accentLineViewWidthConstraint: NSLayoutConstraint! // MARK: - Components private lazy var tabLabels: [UILabel] = tabs.map { tab in let result = UILabel() result.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.text = tab.title result.themeTextColor = .textPrimary result.textAlignment = .center result.alpha = Values.mediumOpacity result.set(.height, to: TabBar.snHeight - Values.separatorThickness - Values.accentLineThickness) return result } private lazy var accentLineView: UIView = { let result = UIView() result.themeBackgroundColor = .primary return result }() // MARK: - Types public struct Tab { let title: String let onTap: () -> Void public init(title: String, onTap: @escaping () -> Void) { self.title = title self.onTap = onTap } } // MARK: - Settings public static let snHeight = isIPhone5OrSmaller ? CGFloat(32) : CGFloat(48) // MARK: - Lifecycle public init(tabs: [Tab]) { self.tabs = tabs super.init(frame: CGRect.zero) setUpViewHierarchy() } public override init(frame: CGRect) { preconditionFailure("Use init(tabs:) instead.") } public required init?(coder: NSCoder) { preconditionFailure("Use init(tabs:) instead.") } private func setUpViewHierarchy() { set(.height, to: TabBar.snHeight) tabLabels.forEach { tabLabel in let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTabLabelTapped(_:))) tabLabel.addGestureRecognizer(tapGestureRecognizer) } let tabLabelStackView = UIStackView(arrangedSubviews: tabLabels) tabLabelStackView.axis = .horizontal tabLabelStackView.distribution = .fillEqually tabLabelStackView.spacing = Values.mediumSpacing let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTabLabelTapped(_:))) tabLabelStackView.addGestureRecognizer(tapGestureRecognizer) tabLabelStackView.set(.height, to: TabBar.snHeight - Values.separatorThickness - Values.accentLineThickness) addSubview(tabLabelStackView) let separator = UIView() separator.themeBackgroundColor = .borderSeparator separator.set(.height, to: Values.separatorThickness) addSubview(separator) accentLineView.set(.height, to: Values.accentLineThickness) addSubview(accentLineView) tabLabelStackView.pin(.leading, to: .leading, of: self) tabLabelStackView.pin(.top, to: .top, of: self) pin(.trailing, to: .trailing, of: tabLabelStackView) separator.pin(.leading, to: .leading, of: self) separator.pin(.top, to: .bottom, of: tabLabelStackView) pin(.trailing, to: .trailing, of: separator) accentLineView.translatesAutoresizingMaskIntoConstraints = false selectTab(at: 0, withAnimatedTransition: false) accentLineView.pin(.top, to: .bottom, of: separator) pin(.bottom, to: .bottom, of: accentLineView) } // MARK: - Updating public func selectTab(at index: Int, withAnimatedTransition isAnimated: Bool = true) { let tabLabel = tabLabels[index] accentLineViewHorizontalCenteringConstraint?.isActive = false accentLineViewHorizontalCenteringConstraint = accentLineView.centerXAnchor.constraint(equalTo: tabLabel.centerXAnchor) accentLineViewHorizontalCenteringConstraint.isActive = true accentLineViewWidthConstraint?.isActive = false accentLineViewWidthConstraint = accentLineView.widthAnchor.constraint(equalTo: tabLabel.widthAnchor) accentLineViewWidthConstraint.isActive = true var tabLabelsCopy = tabLabels tabLabelsCopy.remove(at: index) UIView.animate(withDuration: isAnimated ? 0.25 : 0) { tabLabel.alpha = 1 tabLabelsCopy.forEach { $0.alpha = Values.mediumOpacity } self.layoutIfNeeded() } } // MARK: - Interaction @objc private func handleTabLabelTapped(_ sender: UITapGestureRecognizer) { guard let tabLabel = tabLabels.first(where: { $0.bounds.contains(sender.location(in: $0)) }), let index = tabLabels.firstIndex(of: tabLabel) else { return } selectTab(at: index) let tab = tabs[index] tab.onTap() } }