Tab Bar: Adds tab bar view/view controller

This commit is contained in:
James Magahern
2020-10-28 17:57:34 -07:00
parent af296d7430
commit 5e9c6e5880
7 changed files with 390 additions and 7 deletions

232
App/Tabs/TabBarView.swift Normal file
View File

@@ -0,0 +1,232 @@
//
// TabBarView.swift
// App
//
// Created by James Magahern on 10/28/20.
//
import UIKit
class TabView: UIControl
{
var active: Bool = false { didSet { setNeedsLayout() } }
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.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: insetBounds.height, height: insetBounds.height)
closeButton.frame = CGRect(
x: insetBounds.width - closeButtonSize.width, y: insetBounds.minY,
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: class {
func numberOfTabs(forTabBarView: TabBarView) -> Int
func tabBarView(_ tabBarView: TabBarView, titleForTabAtIndex: Int) -> String
func tabBarView(_ tabBarView: TabBarView, imageForTabAtIndex: Int) -> UIImage?
}
protocol TabBarViewDelegate: class {
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)
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
}
public func reloadTabs() {
guard let dataSource = self.dataSource else { return }
let numberOfTabs = dataSource.numberOfTabs(forTabBarView: self)
while numberOfTabs < tabViews.count {
let tabView = tabViews.removeLast()
tabView.removeFromSuperview()
}
while numberOfTabs > tabViews.count {
tabViews.append(makeTabView())
}
for (i, _) in tabViews.enumerated() {
self.reloadTab(atIndex: i)
}
setNeedsLayout()
}
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 tabView = tabViews[index]
tabView.label.text = title
tabView.imageView.image = image
}
public func activateTab(atIndex index: Int) {
self.activeTabIndex = index
setNeedsLayout()
}
private func makeTabView() -> TabView {
let tabView = TabView()
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 minimumTabWidth = CGFloat(140.0)
let maximumTabWidth = tabContainerBounds.width
var xOffset = CGFloat(0.0)
var tabWidth: CGFloat = (tabContainerBounds.width / CGFloat(tabViews.count))
if tabWidth < minimumTabWidth {
tabWidth = minimumTabWidth
} else if tabWidth > maximumTabWidth {
tabWidth = maximumTabWidth
}
for (i, tabView) in tabViews.enumerated() {
tabContainerView.addSubview(tabView)
tabView.frame = CGRect(
x: xOffset,
y: tabContainerBounds.minY,
width: tabWidth,
height: tabContainerBounds.height
)
xOffset += tabView.frame.width
if i == activeTabIndex {
tabView.active = true
} else {
tabView.active = false
}
}
tabContainerView.contentSize = CGSize(
width: xOffset, height: tabContainerBounds.height
)
}
}

View File

@@ -0,0 +1,74 @@
//
// TabBarViewController.swift
// App
//
// Created by James Magahern on 10/28/20.
//
import Combine
import UIKit
class TabBarViewController: UIViewController, TabBarViewDataSource, TabBarViewDelegate
{
let tabBarView = TabBarView()
private var tabController: TabController
private var tabObserver: AnyCancellable?
private var activeTabIndexObserver: AnyCancellable?
override func loadView() {
self.view = tabBarView
}
init(tabController: TabController) {
self.tabController = tabController
super.init(nibName: nil, bundle: nil)
tabBarView.dataSource = self
tabBarView.delegate = self
tabBarView.reloadTabs()
tabObserver = tabController.$tabs.sink(receiveValue: { [tabBarView] (newTabs: [Tab]) in
DispatchQueue.main.async { tabBarView.reloadTabs() }
})
activeTabIndexObserver = tabController.$activeTabIndex.sink(receiveValue: { [tabBarView] (activeIndex: Int) in
tabBarView.activateTab(atIndex: activeIndex)
})
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: TabBarViewDelegate
func tabBarView(_ tabBarView: TabBarView, didClickToActivateTabAtIndex index: Int) {
tabController.activeTabIndex = index
}
func tabBarView(_ tabBarView: TabBarView, didClickToCloseTabAtIndex index: Int) {
let tab = tabController.tabs[index]
tabController.closeTab(tab)
}
// MARK: TabBarViewDataSource
func numberOfTabs(forTabBarView: TabBarView) -> Int {
return tabController.tabs.count
}
func tabBarView(_ tabBarView: TabBarView, titleForTabAtIndex index: Int) -> String {
let defaultTitle = "New Tab"
if let title = tabController.tabs[index].title, title.count > 0 {
return title
}
return defaultTitle
}
func tabBarView(_ tabBarView: TabBarView, imageForTabAtIndex index: Int) -> UIImage? {
tabController.tabs[index].favicon
}
}

View File

@@ -9,7 +9,9 @@ import Foundation
class TabController
{
var tabs: [Tab] = []
@Published var tabs: [Tab] = []
@Published var activeTabIndex: Int = 0
var policyManager = ResourcePolicyManager()
init() {
@@ -39,6 +41,13 @@ class TabController
func closeTab(_ tab: Tab) {
if let index = tabs.firstIndex(of: tab) {
tabs.remove(at: index)
if tabs.count > 0 {
activeTabIndex = tabs.count - 1
} else {
_ = createNewTab(url: nil)
activeTabIndex = 0
}
}
}
}