Files
Attractor/App/Tabs/TabBarView.swift

317 lines
11 KiB
Swift
Raw Normal View History

//
// TabBarView.swift
// App
//
// Created by James Magahern on 10/28/20.
//
import UIKit
class TabView: UIControl
{
var active: Bool = false { didSet { layoutSubviews() } }
2021-02-11 20:25:16 -08:00
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
2021-02-15 19:21:48 -08:00
imageView.contentMode = .scaleAspectFit
addSubview(imageView)
// 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
)
2021-02-15 19:21:48 -08:00
var xOffset = insetBounds.minX
imageView.frame = CGRect(
x: xOffset, y: insetBounds.minY,
width: insetBounds.height, height: insetBounds.height
)
xOffset += imageView.frame.width
label.frame = CGRect(
2021-02-15 19:21:48 -08:00
x: xOffset + 12.0, y: insetBounds.minY,
width: closeButton.frame.minX - closeButtonPadding - xOffset, 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()
}
}
2021-01-07 12:46:57 -08:00
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
}
2021-01-07 12:46:57 -08:00
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)
2020-10-29 15:28:28 -07:00
private let bottomSeparatorView = UIView(frame: .zero)
2021-02-15 19:21:48 -08:00
private let placeholderTabImage = UIImage(systemName: "network")
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
2020-10-29 15:28:28 -07:00
addSubview(bottomSeparatorView)
bottomSeparatorView.backgroundColor = .systemFill
}
2021-02-11 20:25:16 -08:00
public func reloadTabs(animated: Bool = true) {
guard let dataSource = self.dataSource else { return }
var tabViewsRemoved = Set<TabView>(tabViews)
let numberOfTabs = dataSource.numberOfTabs(forTabBarView: self)
for i in 0..<numberOfTabs {
let identifier = dataSource.tabBarView(self, uniqueIdentifierForTabAtIndex: i)
if let tabView = tabViewsRemoved.first(where: { $0.identifier == identifier }) {
tabViewsRemoved.remove(tabView)
} else {
let newTabView = makeTabView(withIdentifier: identifier)
if animated { newTabView.collapsed = true }
if i < tabViews.count {
tabViews.insert(newTabView, at: i - 1)
} else {
tabViews.append(newTabView)
}
}
}
2021-02-11 20:25:16 -08:00
layoutSubviews()
if animated {
// Incoming tabs uncollapse
2021-02-11 20:25:16 -08:00
tabViews.forEach { $0.collapsed = false }
// Outgoing tabs collapse
tabViewsRemoved.forEach { $0.collapsed = true }
// Reload tabs that are still here
for i in 0..<numberOfTabs {
reloadTab(atIndex: i)
}
UIView.animate(withDuration: 0.22, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: [], animations: { [unowned self] in
2021-02-11 20:25:16 -08:00
layoutSubviews()
}, completion: { [unowned self] finished in
tabViewsRemoved.forEach { $0.removeFromSuperview() }
tabViews.removeAll { tabViewsRemoved.contains($0) }
})
2021-02-11 20:25:16 -08:00
}
// Adjust scroll offset
let contentEnd = CGPoint(x: tabContainerView.contentSize.width - tabContainerView.bounds.width, y: 0.0)
tabContainerView.setContentOffset(contentEnd, animated: animated)
}
public func reloadTab(atIndex index: Int) {
guard let dataSource = self.dataSource else { return }
if index > tabViews.count - 1 { return }
let title = dataSource.tabBarView(self, titleForTabAtIndex: index)
let image = dataSource.tabBarView(self, imageForTabAtIndex: index)
let identifier = dataSource.tabBarView(self, uniqueIdentifierForTabAtIndex: index)
if let tabView = visibleTab(atIndex: index) {
tabView.label.text = title
tabView.identifier = identifier
2021-02-15 19:21:48 -08:00
tabView.imageView.image = image ?? placeholderTabImage
}
}
public func activateTab(atIndex index: Int) {
self.activeTabIndex = index
setNeedsLayout()
}
private func visibleTab(atIndex index: Int) -> TabView? {
let visibleTabs = tabViews.filter { $0.collapsed == false }
if index >= visibleTabs.count { return nil }
return visibleTabs[index]
}
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
2020-10-29 15:28:28 -07:00
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 {
2021-02-11 20:25:16 -08:00
return (tabContainerBounds.width / 3.0) + 15.0
} else {
return 140.0
}
}(traitCollection)
let maximumTabWidth = tabContainerBounds.width
2021-02-11 20:25:16 -08:00
let visibleTabCount = tabViews.filter({ $0.collapsed == false }).count
var xOffset = CGFloat(0.0)
2021-02-11 20:25:16 -08:00
var tabWidth: CGFloat = (tabContainerBounds.width / CGFloat(visibleTabCount))
if tabWidth < minimumTabWidth {
tabWidth = minimumTabWidth
} else if tabWidth > maximumTabWidth {
tabWidth = maximumTabWidth
}
var visibleTabIndex = 0
for tabView in tabViews {
tabContainerView.addSubview(tabView)
tabView.alpha = tabView.collapsed ? 0.0 : 1.0
tabView.frame = CGRect(
x: xOffset,
y: tabContainerBounds.minY,
2021-02-11 20:25:16 -08:00
width: tabView.collapsed ? 1.0 : tabWidth,
height: tabContainerBounds.height
)
xOffset += tabView.frame.width
if visibleTabIndex == activeTabIndex {
tabView.active = true
} else {
tabView.active = false
}
2021-02-11 20:25:16 -08:00
tabView.layoutIfNeeded()
if !tabView.collapsed { visibleTabIndex += 1 }
}
tabContainerView.contentSize = CGSize(
width: xOffset, height: tabContainerBounds.height
)
}
}