vds_ios/VDS/Components/Tabs/Tabs.swift
Matt Bruce 18eec4241b more refactoring
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2023-05-19 15:48:49 -05:00

316 lines
11 KiB
Swift

//
// Tabs.swift
// VDS
//
// Created by Matt Bruce on 5/18/23.
//
import Foundation
import UIKit
import VDSColorTokens
public struct TabModel {
public var text: String
public var onClick: (() -> Void)?
public var width: CGFloat?
public init(text: String, onClick: (() -> Void)? = nil, width: CGFloat? = nil) {
self.text = text
self.onClick = onClick
self.width = width
}
}
public class Tabs: View {
public enum Orientation {
case vertical
case horizontal
}
public enum IndicatorPosition {
case top
case bottom
}
public enum Overflow {
case scroll
case none
}
public enum Size {
case medium
case large
public var textStyle: TextStyle {
if self == .medium {
return .boldBodyLarge
} else {
return .boldTitleSmall
}
}
}
public enum Width {
case percentage(CGFloat)
case value(CGFloat)
}
public var onTabChange: ((Int) -> Void)?
public var orientation: Orientation = .vertical { didSet { setNeedsUpdate() } }
public var borderLine: Bool = true { didSet { setNeedsUpdate() } }
public var fillContainer: Bool = false { didSet { setNeedsUpdate() } }
public var indicatorFillTab: Bool = false { didSet { setNeedsUpdate() } }
public var indicatorPosition: IndicatorPosition = .bottom { didSet { setNeedsUpdate() } }
public var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
public var overflow: Overflow = .scroll { didSet { setNeedsUpdate() } }
public var selectedIndex: Int = 0 { didSet { setNeedsUpdate() } }
public var size: Size = .medium { didSet { setNeedsUpdate() } }
public var sticky: Bool = false { didSet { setNeedsUpdate() } }
public var width: Width = .percentage(0.25) { didSet { setNeedsUpdate() } }
public var tabModels: [TabModel] = [] { didSet { updateTabItems(with: tabModels) } }
private var tabItems: [TabItem] = []
private var tabStackView: UIStackView!
private var scrollView: UIScrollView!
private var contentView: View!
private var borderlineColorConfig = SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark)
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)
}
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(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)
// Add tap gesture recognizer to handle tab selection
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tabItemTapped(_:)))
tabItem.isUserInteractionEnabled = true
tabItem.addGestureRecognizer(tapGesture)
}
setNeedsUpdate()
}
@objc private func tabItemTapped(_ gesture: UITapGestureRecognizer) {
guard let tabItem = gesture.view as? TabItem else { return }
if let selectedIndex = tabItems.firstIndex(of: tabItem) {
self.selectedIndex = selectedIndex
onTabChange?(selectedIndex)
}
}
public override func updateView() {
super.updateView()
if fillContainer {
tabStackView.distribution = .fillEqually
} else {
tabStackView.distribution = .fill
}
tabStackView.axis = orientation == .horizontal ? .horizontal : .vertical
tabStackView.alignment = orientation == .horizontal ? .fill : .leading
tabStackView.spacing = orientation == .horizontal ? VDSLayout.Spacing.space6X.value : VDSLayout.Spacing.space4X.value
setNeedsLayout()
}
public override func layoutSubviews() {
super.layoutSubviews()
// Apply border line
layer.remove(layerName: "borderLineLayer")
// Update tab appearance based on properties
for (index, tabItem) in tabItems.enumerated() {
if selectedIndex == index {
tabItem.selected = true
} else {
tabItem.selected = false
}
tabItem.size = size
tabItem.orientation = orientation
tabItem.surface = surface
}
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: bounds.width - 1, y: 0, width: 1, height: bounds.height)
}
layer.addSublayer(borderLineLayer)
}
// Apply width
if orientation == .vertical {
switch width {
case .percentage(let amount):
contentView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: amount).isActive = true
case .value(let amount):
contentView.widthAnchor.constraint(equalToConstant: amount).isActive = true
}
} else {
// Apply overflow
if orientation == .horizontal && overflow == .scroll {
let contentWidth = tabStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
contentView.widthAnchor.constraint(equalToConstant: contentWidth).isActive = true
} else {
contentView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
}
}
// Enable scrolling if necessary
let contentWidth = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.bounds.height)
//addDebugBorder(color: .blue)
}
}
public class TabItem: View {
public var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } }
public var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } }
public var label: Label = Label()
public var onClick: (() -> Void)? { didSet { setNeedsUpdate() } }
public var width: CGFloat? { didSet { setNeedsUpdate() } }
public var selected: Bool = false { didSet { setNeedsUpdate() } }
public var text: String? { didSet { setNeedsUpdate() } }
private var labelMinWidthConstraint: NSLayoutConstraint?
private var labelWidthConstraint: NSLayoutConstraint?
private var labelLeadingConstraint: NSLayoutConstraint?
private var textColorConfiguration: SurfaceColorConfiguration { selected ? textColorSelectedConfiguration : textColorNonSelectedConfiguration }
private var textColorNonSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOnlight)
private var textColorSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
private var indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteRed, VDSColor.elementsPrimaryOndark)
private var indicatorWidth: CGFloat = 4.0
public override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
backgroundColor = .clear
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
label
.pinTop(5)
.pinTrailing(5)
.pinBottom(6)
labelLeadingConstraint = label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
labelLeadingConstraint?.isActive = true
labelMinWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
labelMinWidthConstraint?.isActive = true
labelWidthConstraint = label.widthAnchor.constraint(equalToConstant: 44.0)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tabItemTapped))
addGestureRecognizer(tapGesture)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required public convenience init() {
self.init(frame: .zero)
}
@objc private func tabItemTapped() {
onClick?()
}
public override func updateView() {
if orientation == .horizontal {
label.textPosition = .center
} else {
label.textPosition = .left
}
label.text = text
label.textStyle = size.textStyle
label.textColor = textColorConfiguration.getColor(self)
if let width {
labelMinWidthConstraint?.isActive = false
labelWidthConstraint?.constant = width
labelWidthConstraint?.isActive = true
} else {
labelWidthConstraint?.isActive = false
labelMinWidthConstraint?.isActive = true
}
if selected {
var indicatorPosition: UIRectEdge = .top
if orientation == .vertical {
indicatorPosition = .left
} else {
if indicatorPosition == .top {
indicatorPosition = .top
} else {
indicatorPosition = .bottom
}
}
addBorder(side: indicatorPosition, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self))
} else {
removeBorders()
}
setNeedsDisplay()
}
// public override func draw(_ rect: CGRect) {
// super.draw(rect)
//
// addDebugBorder(color: .green)
// }
}