Nutrivurv/Nutrivurv/TabBar.swift
//
// TabBar.swift
// Nutrivurv
//
// Created by Dillon on 8/5/20.
// Copyright © 2020 Lambda School. All rights reserved.
import UIKit
public enum TabBarItemPositioning : Int {
case automatic
case fill
case centered
case fillExcludeSeparator
case fillIncludeSeparator
}
internal protocol TabBarDelegate: NSObjectProtocol {
func tabBar(_ tabBar: UITabBar, shouldSelect item: UITabBarItem) -> Bool
func tabBar(_ tabBar: UITabBar, shouldHijack item: UITabBarItem) -> Bool
func tabBar(_ tabBar: UITabBar, didHijack item: UITabBarItem)
}
open class TabBar: UITabBar {
internal weak var customDelegate: TabBarDelegate?
public var itemEdgeInsets = UIEdgeInsets.zero
public var itemCustomPositioning: TabBarItemPositioning? {
didSet {
if let itemCustomPositioning = itemCustomPositioning {
switch itemCustomPositioning {
case .fill:
itemPositioning = .fill
case .automatic:
itemPositioning = .automatic
case .centered:
itemPositioning = .centered
default:
break
}
}
self.reload()
}
}
internal var containers = [TabBarItemContainer]()
internal weak var tabBarController: UITabBarController?
open var moreContentView: TabBarItemContentView? = TabBarItemMoreContentView.init() {
didSet { self.reload() }
}
open override var items: [UITabBarItem]? {
didSet {
self.reload()
}
}
open var isEditing: Bool = false {
didSet {
if oldValue != isEditing {
self.updateLayout()
}
}
}
open override func setItems(_ items: [UITabBarItem]?, animated: Bool) {
super.setItems(items, animated: animated)
self.reload()
}
open override func beginCustomizingItems(_ items: [UITabBarItem]) {
TabBarController.printError("beginCustomizingItems(_:) is unsupported in ESTabBar.")
super.beginCustomizingItems(items)
}
open override func endCustomizing(animated: Bool) -> Bool {
TabBarController.printError("endCustomizing(_:) is unsupported in ESTabBar.")
return super.endCustomizing(animated: animated)
}
open override func layoutSubviews() {
super.layoutSubviews()
self.updateLayout()
}
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var b = super.point(inside: point, with: event)
if !b {
for container in containers {
if container.point(inside: CGPoint.init(x: point.x - container.frame.origin.x, y: point.y - container.frame.origin.y), with: event) {
b = true
}
}
}
return b
}
}
internal extension TabBar /* Layout */ {
func updateLayout() {
guard let tabBarItems = self.items else {
TabBarController.printError("empty items")
return
}
let tabBarButtons = subviews.filter { subview -> Bool in
if let cls = NSClassFromString("UITabBarButton") {
return subview.isKind(of: cls)
}
return false
} .sorted { (subview1, subview2) -> Bool in
return subview1.frame.origin.x < subview2.frame.origin.x
}
if isCustomizing {
for (idx, _) in tabBarItems.enumerated() {
tabBarButtons[idx].isHidden = false
moreContentView?.isHidden = true
}
for (_, container) in containers.enumerated(){
container.isHidden = true
}
} else {
for (idx, item) in tabBarItems.enumerated() {
if let _ = item as? TabBarItem {
tabBarButtons[idx].isHidden = true
} else {
tabBarButtons[idx].isHidden = false
}
if isMoreItem(idx), let _ = moreContentView {
tabBarButtons[idx].isHidden = true
}
}
for (_, container) in containers.enumerated(){
container.isHidden = false
}
}
var layoutBaseSystem = true
if let itemCustomPositioning = itemCustomPositioning {
switch itemCustomPositioning {
case .fill, .automatic, .centered:
break
case .fillIncludeSeparator, .fillExcludeSeparator:
layoutBaseSystem = false
}
}
if layoutBaseSystem {
// System itemPositioning
for (idx, container) in containers.enumerated(){
if !tabBarButtons[idx].frame.isEmpty {
container.frame = tabBarButtons[idx].frame
}
}
} else {
// Custom itemPositioning
var x: CGFloat = itemEdgeInsets.left
var y: CGFloat = itemEdgeInsets.top
switch itemCustomPositioning! {
case .fillExcludeSeparator:
if y <= 0.0 {
y += 1.0
}
default:
break
}
let width = bounds.size.width - itemEdgeInsets.left - itemEdgeInsets.right
let height = bounds.size.height - y - itemEdgeInsets.bottom
let eachWidth = itemWidth == 0.0 ? width / CGFloat(containers.count) : itemWidth
let eachSpacing = itemSpacing == 0.0 ? 0.0 : itemSpacing
for container in containers {
container.frame = CGRect.init(x: x, y: y, width: eachWidth, height: height)
x += eachWidth
x += eachSpacing
}
}
}
}
internal extension TabBar /* Actions */ {
func isMoreItem(_ index: Int) -> Bool {
return TabBarController.isShowingMore(tabBarController) && (index == (items?.count ?? 0) - 1)
}
func removeAll() {
for container in containers {
container.removeFromSuperview()
}
containers.removeAll()
}
func reload() {
removeAll()
guard let tabBarItems = self.items else {
TabBarController.printError("empty items")
return
}
for (idx, item) in tabBarItems.enumerated() {
let container = TabBarItemContainer.init(self, tag: 1000 + idx)
self.addSubview(container)
self.containers.append(container)
if let item = item as? TabBarItem, let contentView = item.contentView {
container.addSubview(contentView)
}
if self.isMoreItem(idx), let moreContentView = moreContentView {
container.addSubview(moreContentView)
}
}
self.updateAccessibilityLabels()
self.setNeedsLayout()
}
@objc func highlightAction(_ sender: AnyObject?) {
guard let container = sender as? TabBarItemContainer else {
return
}
let newIndex = max(0, container.tag - 1000)
guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else {
return
}
if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false {
return
}
if let item = item as? TabBarItem {
item.contentView?.highlight(animated: true, completion: nil)
} else if self.isMoreItem(newIndex) {
moreContentView?.highlight(animated: true, completion: nil)
}
}
@objc func dehighlightAction(_ sender: AnyObject?) {
guard let container = sender as? TabBarItemContainer else {
return
}
let newIndex = max(0, container.tag - 1000)
guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else {
return
}
if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false {
return
}
if let item = item as? TabBarItem {
item.contentView?.dehighlight(animated: true, completion: nil)
} else if self.isMoreItem(newIndex) {
moreContentView?.dehighlight(animated: true, completion: nil)
}
}
@objc func selectAction(_ sender: AnyObject?) {
guard let container = sender as? TabBarItemContainer else {
return
}
select(itemAtIndex: container.tag - 1000, animated: true)
}
@objc func select(itemAtIndex idx: Int, animated: Bool) {
let newIndex = max(0, idx)
let currentIndex = (selectedItem != nil) ? (items?.firstIndex(of: selectedItem!) ?? -1) : -1
guard newIndex < items?.count ?? 0, let item = self.items?[newIndex], item.isEnabled == true else {
return
}
if (customDelegate?.tabBar(self, shouldSelect: item) ?? true) == false {
return
}
if (customDelegate?.tabBar(self, shouldHijack: item) ?? false) == true {
customDelegate?.tabBar(self, didHijack: item)
if animated {
if let item = item as? TabBarItem {
item.contentView?.select(animated: animated, completion: {
item.contentView?.deselect(animated: false, completion: nil)
})
} else if self.isMoreItem(newIndex) {
moreContentView?.select(animated: animated, completion: {
self.moreContentView?.deselect(animated: animated, completion: nil)
})
}
}
return
}
if currentIndex != newIndex {
if currentIndex != -1 && currentIndex < items?.count ?? 0{
if let currentItem = items?[currentIndex] as? TabBarItem {
currentItem.contentView?.deselect(animated: animated, completion: nil)
} else if self.isMoreItem(currentIndex) {
moreContentView?.deselect(animated: animated, completion: nil)
}
}
if let item = item as? TabBarItem {
item.contentView?.select(animated: animated, completion: nil)
} else if self.isMoreItem(newIndex) {
moreContentView?.select(animated: animated, completion: nil)
}
} else if currentIndex == newIndex {
if let item = item as? TabBarItem {
item.contentView?.reselect(animated: animated, completion: nil)
} else if self.isMoreItem(newIndex) {
moreContentView?.reselect(animated: animated, completion: nil)
}
if let tabBarController = tabBarController {
var navVC: UINavigationController?
if let n = tabBarController.selectedViewController as? UINavigationController {
navVC = n
} else if let n = tabBarController.selectedViewController?.navigationController {
navVC = n
}
if let navVC = navVC {
if navVC.viewControllers.contains(tabBarController) {
if navVC.viewControllers.count > 1 && navVC.viewControllers.last != tabBarController {
navVC.popToViewController(tabBarController, animated: true);
}
} else {
if navVC.viewControllers.count > 1 {
navVC.popToRootViewController(animated: animated)
}
}
}
}
}
delegate?.tabBar?(self, didSelect: item)
self.updateAccessibilityLabels()
}
func updateAccessibilityLabels() {
guard let tabBarItems = self.items, tabBarItems.count == self.containers.count else {
return
}
for (idx, item) in tabBarItems.enumerated() {
let container = self.containers[idx]
container.accessibilityIdentifier = item.accessibilityIdentifier
container.accessibilityTraits = item.accessibilityTraits
if item == selectedItem {
container.accessibilityTraits = container.accessibilityTraits.union(.selected)
}
if let explicitLabel = item.accessibilityLabel {
container.accessibilityLabel = explicitLabel
container.accessibilityHint = item.accessibilityHint ?? container.accessibilityHint
} else {
var accessibilityTitle = ""
if let item = item as? TabBarItem {
accessibilityTitle = item.accessibilityLabel ?? item.title ?? ""
}
if self.isMoreItem(idx) {
accessibilityTitle = NSLocalizedString("More_TabBarItem", bundle: Bundle(for:TabBarController.self), comment: "")
}
let formatString = NSLocalizedString(item == selectedItem ? "TabBarItem_Selected_AccessibilityLabel" : "TabBarItem_AccessibilityLabel",
bundle: Bundle(for: TabBarController.self),
comment: "")
container.accessibilityLabel = String(format: formatString, accessibilityTitle, idx + 1, tabBarItems.count)
}
}
}
}