vds_ios/VDS/Components/Tabs/Tabs.swift
Matt Bruce 7fa75eb519 commented out stuff dealing with width since this doesn't apply to this view for now
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2023-05-24 17:40:34 -05:00

330 lines
12 KiB
Swift

//
// Tabs.swift
// VDS
//
// Created by Matt Bruce on 5/18/23.
//
import Foundation
import UIKit
import VDSColorTokens
open class Tabs: View {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
public enum Orientation: String, CaseIterable{
case vertical
case horizontal
}
public enum IndicatorPosition: String, CaseIterable {
case top
case bottom
var value: UIRectEdge {
if self == .top {
return .top
} else {
return .bottom
}
}
}
public enum Overflow: String, CaseIterable {
case scroll
case none
}
public enum Size: String, CaseIterable {
case medium
case large
public var textStyle: TextStyle {
if self == .medium {
return .boldBodyLarge
} else {
//specs show that the font size shouldn't change however boldTitleSmall does
//change point size between iPad/iPhone. This is a "fix" so each device will
//load the correct pointSize
return UIDevice.isIPad ? .boldTitleSmall : .boldTitleMedium
}
}
}
public enum Width {
case percentage(CGFloat)
case value(CGFloat)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
///An optional callback that is called when the selectedIndex changes. Passes parameters (event, tabIndex).
open var onTabChange: ((Int) -> Void)?
//Determines the layout of the Tabs, defaults to horizontal
open var orientation: Orientation = .horizontal { didSet { if oldValue != orientation { updateTabItems() } } }
///When true, Tabs will have border line. If false is passed then the border line won't be visible.
open var borderLine: Bool = true { didSet { setNeedsUpdate() } }
///It will fill the Tabs to the width of the compoent and all Tabs will be in equal width when orientation is horizontal. This is recommended when there are no more than 2-3 tabs.
open var fillContainer: Bool = false { didSet { updateTabItems() } }
///When true, Tabs will be sticky to top of page, when orientation is vertical.
open var indicatorFillTab: Bool = false { didSet { setNeedsUpdate() } }
///Sets the Position of the Selected/Hover Border Accent for All Tabs, only for Horizontal Orientation
open var indicatorPosition: IndicatorPosition = .bottom { didSet { setNeedsUpdate() } }
///Minimum Width for All Tabs, when orientation is horizontal.
open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
///If set to 'scroll', Tabs can be overflow and scrollable. With 'none', tabs will not overflow and labels will be wrapped to multiple lines if the label text is long.
open var overflow: Overflow = .scroll { didSet { updateTabItems() } }
///The initial Selected Tab's index and is set once a Tab is clicked
open var selectedIndex: Int = 0 { didSet { setNeedsUpdate() } }
///Determines the size of the Tabs TextStyle
open var size: Size = .medium { didSet { updateTabItems() } }
///When true, Tabs will be sticky to top of page, when orientation is vertical.
open var sticky: Bool = false { didSet { setNeedsUpdate() } }
///Model of the Tabs you are wanting to show.
open var tabModels: [TabModel] = [] { didSet { updateTabItems() } }
// //rules for width
// private var _width: Width? = .percentage(0.25)
//
// ///Width of all Tabs when orientation is vertical, defaults to 25%.
// open var width: Width? {
// get {
// return _width
// }
// set {
// if let newValue {
// switch newValue {
// case .percentage(let percentage):
// if percentage >= 0 && percentage <= 1 {
// _width = newValue
// setNeedsUpdate()
// } else {
// print("Invalid percentage value. It should be between 0 and 1.")
// }
// case .value(let value):
// if value >= minWidth {
// _width = newValue
// setNeedsUpdate()
// } else {
// print("Invalid value. It should be greater than or equal to \(minWidth).")
// }
// }
// }
// }
// }
open var tabItems: [TabItem] = []
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var tabStackView: UIStackView!
private var scrollView: UIScrollView!
private var contentView: View!
private var borderlineColorConfig = SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark)
private var contentViewWidthConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
public convenience required init() {
self.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func setup() {
super.setup()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
addSubview(scrollView)
contentView = View()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
tabStackView = UIStackView()
tabStackView.axis = .horizontal
tabStackView.distribution = .fill
tabStackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(tabStackView)
scrollView.pinToSuperView()
contentView.pinToSuperView()
tabStackView.pinToSuperView()
contentView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
}
private func updateTabItems() {
updateTabItems(with: tabModels)
}
private func updateTabItems(with models: [TabModel]) {
// Clear existing tab items
for tabItem in tabItems {
tabItem.removeFromSuperview()
}
tabItems.removeAll()
// Create new tab items from the models
for model in models {
let tabItem = TabItem()
tabItem.text = model.text
tabItem.onClick = model.onClick
tabItem.width = model.width
tabItems.append(tabItem)
tabStackView.addArrangedSubview(tabItem)
tabItem
.publisher(for: UITapGestureRecognizer())
.sink { [weak self] gesture in
guard let self, let tabItem = gesture.view as? TabItem else { return }
if let selectedIndex = tabItems.firstIndex(of: tabItem) {
self.selectedIndex = selectedIndex
self.onTabChange?(selectedIndex)
}
}.store(in: &tabItem.subscribers)
}
setNeedsUpdate()
scrollToSelectedIndex(animated: false)
}
private func scrollToSelectedIndex(animated: Bool) {
if orientation == .horizontal && self.overflow == .scroll, selectedIndex < tabItems.count {
let selectedTab = tabItems[selectedIndex]
scrollView.scrollRectToVisible(selectedTab.frame, animated: animated)
}
}
open override func updateView() {
super.updateView()
// Update the stackview properties
if orientation == .horizontal && fillContainer {
tabStackView.distribution = .fillEqually
} else {
tabStackView.distribution = .fillProportionally
}
tabStackView.axis = orientation == .horizontal ? .horizontal : .vertical
tabStackView.alignment = orientation == .horizontal ? .fill : .leading
tabStackView.spacing = orientation == .horizontal ? VDSLayout.Spacing.space6X.value : VDSLayout.Spacing.space4X.value
// Update tab appearance based on properties
for (index, tabItem) in tabItems.enumerated() {
tabItem.selected = selectedIndex == index
tabItem.index = index
tabItem.minWidth = minWidth
tabItem.size = size
tabItem.textPosition = orientation == .horizontal && fillContainer ? .center : .left
tabItem.orientation = orientation
tabItem.surface = surface
tabItem.indicatorPosition = indicatorPosition
}
// Deactivate old constraint
contentViewWidthConstraint?.isActive = false
// Apply width
if orientation == .vertical {
scrollView.isScrollEnabled = false
// switch width {
// case .percentage(let amount):
// contentViewWidthConstraint = contentView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: amount)
// case .value(let amount):
// contentViewWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: amount)
// }
contentViewWidthConstraint = contentView.widthAnchor.constraint(equalTo: widthAnchor)
} else {
// Apply overflow
if overflow == .scroll && !fillContainer {
let contentWidth = tabStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
contentViewWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: contentWidth)
// Enable scrolling if necessary
scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.bounds.height)
} else {
contentViewWidthConstraint = contentView.widthAnchor.constraint(equalTo: widthAnchor)
}
}
scrollView.isScrollEnabled = orientation == .horizontal && overflow == .scroll
// Activate old constraint
contentViewWidthConstraint?.isActive = true
setNeedsLayout()
layoutIfNeeded()
}
open override func layoutSubviews() {
super.layoutSubviews()
//scrollView Contentsize
if orientation == .horizontal && overflow == .scroll && !fillContainer {
let contentWidth = tabStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.bounds.height)
} else {
scrollView.contentSize = bounds.size
}
// Apply border line
layer.remove(layerName: "borderLineLayer")
if borderLine {
let borderLineLayer = CALayer()
borderLineLayer.name = "borderLineLayer"
borderLineLayer.backgroundColor = borderlineColorConfig.getColor(self).cgColor
if orientation == .horizontal {
borderLineLayer.frame = CGRect(x: 0, y: bounds.height - 1, width: bounds.width, height: 1)
} else {
borderLineLayer.frame = CGRect(x: 0, y: 0, width: 1, height: bounds.height)
}
layer.addSublayer(borderLineLayer)
}
scrollToSelectedIndex(animated: true)
}
// //--------------------------------------------------
// // MARK: - Private Methods
// //--------------------------------------------------
// private func tabWidth(for item: TabItem) -> CGFloat? {
// guard orientation == .vertical else { return item.width }
// var calculated: CGFloat
// switch width {
// case .percentage(let percent):
// calculated = (bounds.width * percent) - tabStackView.spacing
// case .value(let value):
// calculated = value - tabStackView.spacing
// }
//
// return calculated > minWidth ? calculated : minWidth
// }
}