// // TabBarView.swift // App // // Created by James Magahern on 10/28/20. // import UIKit class TabView: UIControl { var active: Bool = false { didSet { setNeedsLayout() } } var collapsed: Bool = false var identifier: UUID? let label = UILabel(frame: .zero) let closeButton = UIButton(frame: .zero) let imageView = UIImageView(image: nil) private let leftSeparator = UIView(frame: .zero) private let rightSeparator = UIView(frame: .zero) convenience init() { self.init(frame: .zero) addSubview(label) label.text = "Tab View" label.font = .boldSystemFont(ofSize: 11.0) addSubview(closeButton) closeButton.setImage(UIImage(systemName: "xmark.square.fill"), for: .normal) closeButton.imageView?.contentMode = .center closeButton.tintColor = .label addSubview(leftSeparator) addSubview(rightSeparator) leftSeparator.backgroundColor = .secondarySystemFill rightSeparator.backgroundColor = .secondarySystemFill // Try just one for now leftSeparator.isHidden = true } override func layoutSubviews() { super.layoutSubviews() let insetBounds = bounds.inset(by: layoutMargins) let closeButtonPadding = CGFloat(5.0) let closeButtonSize = CGSize(width: bounds.height, height: bounds.height) closeButton.frame = CGRect( x: insetBounds.width - closeButtonSize.height, y: 0.0, width: closeButtonSize.width, height: closeButtonSize.height ) label.frame = CGRect( x: insetBounds.minX, y: insetBounds.minY, width: closeButton.frame.minX - closeButtonPadding, height: insetBounds.height ) let separatorWidth = CGFloat(1.0) leftSeparator.frame = CGRect( x: 0.0, y: 0.0, width: separatorWidth, height: bounds.height ) rightSeparator.frame = CGRect( x: bounds.width - separatorWidth, y: 0.0, width: separatorWidth, height: bounds.height ) if isTracking { backgroundColor = .systemFill } else if active { backgroundColor = .init(dynamicProvider: { (traitCollection) -> UIColor in if traitCollection.userInterfaceStyle == .light { return .secondarySystemGroupedBackground } else { return .secondarySystemFill } }) } else { backgroundColor = .init(dynamicProvider: { (traitCollection) -> UIColor in if traitCollection.userInterfaceStyle == .light { return .secondarySystemFill } else { return .secondarySystemGroupedBackground } }) } } override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { super.beginTracking(touch, with: event) setNeedsLayout() return true } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { super.endTracking(touch, with: event) setNeedsLayout() } } protocol TabBarViewDataSource: AnyObject { func numberOfTabs(forTabBarView: TabBarView) -> Int func tabBarView(_ tabBarView: TabBarView, titleForTabAtIndex: Int) -> String func tabBarView(_ tabBarView: TabBarView, imageForTabAtIndex: Int) -> UIImage? func tabBarView(_ tabBarView: TabBarView, uniqueIdentifierForTabAtIndex: Int) -> UUID } protocol TabBarViewDelegate: AnyObject { func tabBarView(_ tabBarView: TabBarView, didClickToActivateTabAtIndex: Int) func tabBarView(_ tabBarView: TabBarView, didClickToCloseTabAtIndex: Int) } class TabBarView: UIView { static let preferredHeight: CGFloat = 30.0 public var delegate: TabBarViewDelegate? public var dataSource: TabBarViewDataSource? private var tabViews: [TabView] = [] private var activeTabIndex: Int = 0 private var tabContainerView = UIScrollView(frame: .zero) private let bottomSeparatorView = UIView(frame: .zero) override func sizeThatFits(_ size: CGSize) -> CGSize { CGSize(width: size.width, height: Self.preferredHeight) } convenience init() { self.init(frame: .zero) addSubview(tabContainerView) backgroundColor = .secondarySystemGroupedBackground tabContainerView.showsHorizontalScrollIndicator = false tabContainerView.showsVerticalScrollIndicator = false addSubview(bottomSeparatorView) bottomSeparatorView.backgroundColor = .systemFill } public func reloadTabs(animated: Bool = true) { guard let dataSource = self.dataSource else { return } var tabViewsRemoved = Set(tabViews) let numberOfTabs = dataSource.numberOfTabs(forTabBarView: self) for i in 0.. tabViews.count - 1 { return } let title = dataSource.tabBarView(self, titleForTabAtIndex: index) let image = dataSource.tabBarView(self, imageForTabAtIndex: index) let tabView = tabViews[index] tabView.label.text = title tabView.imageView.image = image } public func activateTab(atIndex index: Int) { self.activeTabIndex = index setNeedsLayout() } private func makeTabView(withIdentifier identifier: UUID) -> TabView { let tabView = TabView() tabView.identifier = identifier tabView.addAction(UIAction(handler: { [unowned self, tabView] _ in guard let delegate = self.delegate else { return } guard let tabIndex = self.tabViews.firstIndex(of: tabView) else { return } delegate.tabBarView(self, didClickToActivateTabAtIndex: tabIndex) }), for: .touchUpInside) tabView.closeButton.addAction(UIAction(handler: { [unowned self, tabView] _ in guard let delegate = self.delegate else { return } guard let tabIndex = self.tabViews.firstIndex(of: tabView) else { return } delegate.tabBarView(self, didClickToCloseTabAtIndex: tabIndex) }), for: .touchUpInside) return tabView } override func layoutSubviews() { super.layoutSubviews() let tabContainerBounds = bounds tabContainerView.frame = tabContainerBounds let separatorHeight = CGFloat(1.0) bottomSeparatorView.frame = CGRect( x: 0.0, y: bounds.height - separatorHeight, width: bounds.width, height: separatorHeight ) let minimumTabWidth = { (traitCollection: UITraitCollection) -> CGFloat in if traitCollection.horizontalSizeClass == .compact { return (tabContainerBounds.width / 3.0) + 15.0 } else { return 140.0 } }(traitCollection) let maximumTabWidth = tabContainerBounds.width let visibleTabCount = tabViews.filter({ $0.collapsed == false }).count var xOffset = CGFloat(0.0) var tabWidth: CGFloat = (tabContainerBounds.width / CGFloat(visibleTabCount)) if tabWidth < minimumTabWidth { tabWidth = minimumTabWidth } else if tabWidth > maximumTabWidth { tabWidth = maximumTabWidth } for (i, tabView) in tabViews.enumerated() { tabContainerView.addSubview(tabView) tabView.alpha = tabView.collapsed ? 0.0 : 1.0 tabView.frame = CGRect( x: xOffset, y: tabContainerBounds.minY, width: tabView.collapsed ? 1.0 : tabWidth, height: tabContainerBounds.height ) xOffset += tabView.frame.width if i == activeTabIndex { tabView.active = true } else { tabView.active = false } tabView.layoutIfNeeded() } tabContainerView.contentSize = CGSize( width: xOffset, height: tabContainerBounds.height ) } }