diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 174e2101..828585b1 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -43,6 +43,8 @@ EA4DB18528CA967F00103EE3 /* SelectorGroupHandlerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */; }; EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */; }; EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB30128DCBCA500103EE3 /* Badge.swift */; }; + EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA596ABC2A16B4EC00300C4B /* Tab.swift */; }; + EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA596ABE2A16B4F500300C4B /* Tabs.swift */; }; EA5E304C294CBDD00082B959 /* TileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E304B294CBDD00082B959 /* TileContainer.swift */; }; EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E30522950DDA60082B959 /* TitleLockup.swift */; }; EA5E3058295105A40082B959 /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E3057295105A40082B959 /* Tilelet.swift */; }; @@ -164,6 +166,8 @@ EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorGroupHandlerBase.swift; sourceTree = ""; }; EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEquatable.swift; sourceTree = ""; }; EA4DB30128DCBCA500103EE3 /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; }; + EA596ABC2A16B4EC00300C4B /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; + EA596ABE2A16B4F500300C4B /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = ""; }; EA5E304B294CBDD00082B959 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = ""; }; EA5E30522950DDA60082B959 /* TitleLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockup.swift; sourceTree = ""; }; EA5E3057295105A40082B959 /* Tilelet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilelet.swift; sourceTree = ""; }; @@ -384,6 +388,7 @@ EA89200B28B530F0006B9984 /* RadioBox */, EAF7F11428A1470D00B287F5 /* RadioButton */, EA1F265F28B945070033E859 /* RadioSwatch */, + EA596ABB2A16B4D500300C4B /* Tabs */, EAC925852911C9DE00091998 /* TextFields */, EA5E304A294CBDBB0082B959 /* TileContainer */, EA5E3056295105930082B959 /* Tilelet */, @@ -511,6 +516,15 @@ path = Badge; sourceTree = ""; }; + EA596ABB2A16B4D500300C4B /* Tabs */ = { + isa = PBXGroup; + children = ( + EA596ABC2A16B4EC00300C4B /* Tab.swift */, + EA596ABE2A16B4F500300C4B /* Tabs.swift */, + ); + path = Tabs; + sourceTree = ""; + }; EA5E304A294CBDBB0082B959 /* TileContainer */ = { isa = PBXGroup; children = ( @@ -827,6 +841,7 @@ EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */, EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, + EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */, EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */, EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */, @@ -893,6 +908,7 @@ EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EAC9257D29119B5400091998 /* TextLink.swift in Sources */, EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */, + EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */, 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, ); diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index f713777f..ae137257 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -6,3 +6,426 @@ // import Foundation +import VDSColorTokens + +import Combine + +//@objc(VDSCollectionViewCell) +//open class CollectionViewCell: UICollectionViewCell, Handlerable, ViewProtocol, Resettable { +// +// //-------------------------------------------------- +// // MARK: - Combine Properties +// //-------------------------------------------------- +// public var subscribers = Set() +// +// //-------------------------------------------------- +// // MARK: - Properties +// //-------------------------------------------------- +// private var initialSetupPerformed = false +// +// open var surface: Surface = .light { didSet { setNeedsUpdate() }} +// +// open var disabled: Bool = false { didSet { setNeedsUpdate() } } +// +// public var shouldUpdateView: Bool = true +// +// //-------------------------------------------------- +// // MARK: - Initializers +// //-------------------------------------------------- +// required public init() { +// super.init(frame: .zero) +// initialSetup() +// } +// +// public override init(frame: CGRect) { +// super.init(frame: .zero) +// initialSetup() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// initialSetup() +// } +// +// //-------------------------------------------------- +// // MARK: - Public Functions +// //-------------------------------------------------- +// open func initialSetup() { +// if !initialSetupPerformed { +// setup() +// setNeedsUpdate() +// } +// } +// +// open func setup() {} +// +// open func reset() {} +// +// open func updateView() { +// updateAccessibilityLabel() +// } +// +// open func updateAccessibilityLabel() {} +// +// open override func prepareForReuse() { +// surface = .light +// disabled = false +// } +//} +// +// +//@objc(VDSTabsDelegate) +//public protocol TabsDelegate { +// func shouldSelectItem(_ indexPath: IndexPath, tabs: Tabs) -> Bool +// func didSelectItem(_ indexPath: IndexPath, tabs: Tabs) +//} +// +//@objc(VDSTabs) +//open class Tabs: View { +// +// private let layout = UICollectionViewFlowLayout() +// public lazy var collectionView: UICollectionView = { +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) +// collectionView.translatesAutoresizingMaskIntoConstraints = false +// collectionView.register(TabItemCell.self, forCellWithReuseIdentifier: TabCellId) +// collectionView.backgroundColor = .clear +// collectionView.showsVerticalScrollIndicator = false +// collectionView.showsHorizontalScrollIndicator = false +// collectionView.dataSource = self +// collectionView.delegate = self +// return collectionView +// }() +// +// open var tabItemModels = [TabItemModel]() +// +// let bottomScrollView = UIScrollView(frame: .zero) +// let bottomContentView = View() +// let bottomLine = Line() +// let selectionLine = View() +// var selectionLineLeadingConstraint: NSLayoutConstraint? +// var selectionLineWidthConstraint: NSLayoutConstraint? +// +// private var widthCell = TabItemCell() +// +// //delegate +// weak public var delegate: TabsDelegate? +// +// //control var +// public var selectedIndex: Int = 0 +// public var paddingBeforeFirstTab: Bool = true +// +// //constant +// let TabCellId = "TabCell" +// public let itemSpacing: CGFloat = 20.0 +// public let cellHeight: CGFloat = 28.0 +// public let selectionLineHeight: CGFloat = 4.0 +// public let minimumItemWidth: CGFloat = 32.0 +// public let selectionLineMovingTime: TimeInterval = 0.2 +// +// //------------------------------------------------- +// // MARK:- Layout Views +// //------------------------------------------------- +// +// open override func reset() { +// super.reset() +// selectedIndex = 0 +// paddingBeforeFirstTab = true +// } +// +// open override func setup() { +// super.setup() +// backgroundColor = VDSColor.backgroundPrimaryLight +// addSubview(bottomLine) +// setupCollectionView() +// setupSelectionLine() +// setupConstraints() +// } +// +// func setupCollectionView () { +// layout.scrollDirection = .horizontal +// layout.minimumLineSpacing = 0 +// addSubview(collectionView) +// } +// +// func setupSelectionLine() { +// bottomScrollView.translatesAutoresizingMaskIntoConstraints = false +// bottomScrollView.delegate = self +// addSubview(bottomScrollView) +// bottomScrollView.addSubview(bottomContentView) +// selectionLine.backgroundColor = VDSColor.paletteRed +// bottomContentView.addSubview(selectionLine) +// bringSubviewToFront(bottomScrollView) +// } +// +// func setupConstraints() { +// //collection view +// collectionView +// .pinTop() +// .pinLeading() +// .pinTrailing() +// +// collectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: cellHeight).isActive = true +// +// //selection line +// bottomScrollView.heightAnchor.constraint(equalToConstant: selectionLineHeight).isActive = true +// bottomScrollView.topAnchor.constraint(equalTo: collectionView.bottomAnchor).isActive = true +// bottomScrollView +// .pinLeading() +// .pinTrailing() +// +// selectionLine.heightAnchor.constraint(equalToConstant: selectionLineHeight).isActive = true +// selectionLine +// .pinTop() +// .pinBottom() +// +// selectionLineLeadingConstraint = selectionLine.leadingAnchor.constraint(equalTo: bottomContentView.leadingAnchor) +// selectionLineLeadingConstraint?.isActive = true +// selectionLineWidthConstraint = selectionLine.widthAnchor.constraint(equalToConstant: minimumItemWidth) +// selectionLineWidthConstraint?.isActive = true +// +// bottomContentView.pinToSuperView() +// +// //bottom line +// bottomLine.topAnchor.constraint(equalTo: bottomScrollView.bottomAnchor).isActive = true +// bottomLine +// .pinBottom() +// .pinLeading() +// .pinTrailing() +// } +// +// //------------------------------------------------- +// // MARK:- Control Methods +// //------------------------------------------------- +// +// public func selectIndex(_ index: Int, animated: Bool) { +// guard tabItemModels.count > 0 else { +// selectedIndex = index +// return +// } +// +// DispatchQueue.main.async { [weak self] in +// guard let self else { return } +// let currentIndex = self.selectedIndex +// self.selectedIndex = index +// self.deselect(indexPath: IndexPath(row: currentIndex, section: 0)) +// self.selectItem(atIndexPath: IndexPath(row: index, section: 0), animated: animated) +// } +// } +// +// public func reloadData() { +// collectionView.reloadData() +// } +//} +// +////------------------------------------------------- +//// MARK:- Collection View Methods +////------------------------------------------------- +// +//extension Tabs: UICollectionViewDataSource { +// public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { +// tabItemModels.count +// } +// +// public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { +// guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCellId, for: indexPath) as? TabItemCell else { +// return UICollectionViewCell() +// } +// let model = tabItemModels[indexPath.row] +// cell.tabsCount = tabItemModels.count +// cell.tabSelected = indexPath.row == selectedIndex +// cell.text = model.text +// cell.onClick = model.onClick +// return cell +// } +//} +// +//extension Tabs: UICollectionViewDelegateFlowLayout { +// +// public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { +// guard self.collectionView(collectionView, numberOfItemsInSection: indexPath.section) != 2 else { +// // If two tabs, take up the screen +// let insets = self.collectionView(collectionView, layout: collectionViewLayout, insetForSectionAt: indexPath.section) +// let width = (collectionView.bounds.width / 2.0) - insets.left - insets.right +// return CGSize(width: width, height: cellHeight) +// } +// return CGSize(width: max(minimumItemWidth, getLabelWidth(tabItemModels[indexPath.row]).width), height: cellHeight) +// } +// +// //pre calculate the width of the collection cell +// //when user select tabs, it will reload related collectionview, if we use autosize, it would relayout the width, need to keep the cell width constant. +// func getLabelWidth(_ model: TabItemModel) -> CGSize { +// widthCell.text = model.text +// let cgSize = widthCell.label.intrinsicContentSize +// widthCell.label.reset() +// return cgSize +// } +// +// public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { +// guard section == 0 else { +// return UIEdgeInsets(top: 0, left: itemSpacing, bottom: 0, right: 0) +// } +// guard paddingBeforeFirstTab else { +// return .zero +// } +// return UIEdgeInsets(top: 0, left: VDSLayout.Spacing.space6X.value, bottom: 0, right: 0) +// } +// +// public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { +// // If two tabs, take up the screen, no space between items +// guard self.collectionView(collectionView, numberOfItemsInSection: section) != 2 else { +// return 0 +// } +// return itemSpacing +// } +// +// public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { +// return delegate?.shouldSelectItem(indexPath, tabs: self) ?? true +// } +// +// public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { +// selectIndex(indexPath.row, animated: true) +// } +// +// public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { +// guard let tabCell = cell as? TabItemCell else { return } +// if indexPath.row == selectedIndex { +// DispatchQueue.main.async { +// self.moveSelectionLine(toIndex: indexPath, animated: false, cell: tabCell) +// } +// } +// } +// +// func deselect(indexPath:IndexPath) { +// collectionView.deselectItem(at: indexPath, animated: false) +// collectionView.reloadItems(at: [indexPath]) +// } +// +// func selectItem(atIndexPath indexPath: IndexPath, animated: Bool) { +// +// guard tabItemModels.count > 0 else { return } +// +// collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .centeredHorizontally) +// guard let tabCell = collectionView.cellForItem(at: indexPath) as? TabItemCell else { return } +// moveSelectionLine(toIndex: indexPath, animated: animated, cell: tabCell) +// tabCell.tabSelected = true +// tabCell.setNeedsDisplay() +// tabCell.setNeedsLayout() +// tabCell.layoutIfNeeded() +// if let delegate = delegate { +// delegate.didSelectItem(indexPath, tabs: self) +// } else if let action = tabItemModels[selectedIndex].onClick { +// action() +// } +// if UIAccessibility.isVoiceOverRunning { +// UIAccessibility.post(notification: .layoutChanged, argument: tabCell) +// } +// } +//} +// +// +//extension Tabs: UIScrollViewDelegate { +// public func scrollViewDidScroll(_ scrollView: UIScrollView) { +// /*bottomScrollview is subview of self, it's not belongs to collectionview. +// When collectionview is scrolling, bottomScrollView will stay without moving +// Adding collectionview's offset to bottomScrollView, will make the bottomScrollview looks like scrolling with the selected tab item. +// */ +// let offsetX = collectionView.contentOffset.x +// bottomScrollView.setContentOffset(CGPoint(x: offsetX, y: bottomScrollView.contentOffset.y), animated: false) +// } +//} +// +// +////------------------------------------------------- +//// MARK:- Bottom Line Methods +////------------------------------------------------- +//extension Tabs { +// func moveSelectionLine(toIndex indexPath: IndexPath, animated: Bool, cell: TabItemCell) { +// let size = collectionView(collectionView, layout: layout, sizeForItemAt: indexPath) +// let animationBlock = { +// [weak self] in +// self?.selectionLineWidthConstraint?.constant = size.width +// self?.selectionLineLeadingConstraint?.constant = cell.frame.origin.x +// self?.bottomContentView.layoutIfNeeded() +// } +// if animated { +// UIView.animate(withDuration: selectionLineMovingTime, animations: animationBlock) +// } else { +// animationBlock() +// } +// } +// +// /// Adjust the line based on the percentage +// func progress(from index: Int, toIndex: Int, percentage: CGFloat) { +// let fromIndexPath = IndexPath(row: index, section: 0) +// let toIndexPath = IndexPath(row: toIndex, section: 0) +// guard let fromCell = collectionView.cellForItem(at: fromIndexPath), +// let toCell = collectionView.cellForItem(at: toIndexPath) else { return } +// +// // setting the width for percentage +// selectionLineWidthConstraint?.constant = (toCell.bounds.width - fromCell.bounds.width) * percentage + fromCell.bounds.width +// +// // setting the x for percentage +// let originalX = fromCell.frame.origin.x +// let toX = toCell.frame.origin.x +// let xDifference = toX - originalX +// let finalX = (xDifference * percentage) + originalX +// selectionLineLeadingConstraint?.constant = finalX +// +// bottomContentView.layoutIfNeeded() +// } +//} +// +//@objc(VDSTabItemCell) +//open class TabItemCell: CollectionViewCell { +// //-------------------------------------------------- +// // MARK: - Properties +// //-------------------------------------------------- +// open var label = Label() +// open var text: String = "" { didSet { setNeedsUpdate() }} +// open var textStyle: TextStyle = .bodyLarge { didSet { setNeedsUpdate() }} +// open var tabSelected: Bool = false { didSet { setNeedsUpdate() }} +// open var tabsCount: Int = 0 { didSet { setNeedsUpdate() }} +// open var onClick: (()->())? +// +// open override func setup() { +// super.setup() +// contentView.addSubview(label) +// label +// .pinLeading() +// .pinTrailing() +// .pinBottom(6) +// label.baselineAdjustment = .alignCenters +// } +// +// open var selectedColorConfiguration: AnyColorable = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark).eraseToAnyColorable() +// +// open var unSelectedColorConfiguration: AnyColorable = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark).eraseToAnyColorable() +// +// open override func updateView() { +// super.updateView() +// label.text = text +// label.textStyle = textStyle +// label.surface = surface +// label.textColorConfiguration = tabSelected ? selectedColorConfiguration : unSelectedColorConfiguration +// } +// +// open override func reset() { +// super.reset() +// label.reset() +// label.textStyle = .bodyLarge +// } +// +// open override func updateAccessibilityLabel() { +// +// } +//} +// +//public struct TabItemModel { +// public var text: String +// public var onClick: (() -> ())? +// +// public init(text: String, onClick: (() -> Void)? = nil) { +// self.text = text +// self.onClick = onClick +// } +//} diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 7c503f30..761c41d9 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -6,3 +6,349 @@ // import Foundation +import UIKit +import VDSColorTokens + +public struct TabItemModel { + 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 orientation: Orientation = .horizontal { + didSet { setNeedsLayout() } + } + + public var borderLine: Bool = true { + didSet { setNeedsLayout() } + } + + public var fillContainer: Bool = true { + didSet { setNeedsLayout() } + } + + public var indicatorFillTab: Bool = false { + didSet { setNeedsLayout() } + } + + public var indicatorPosition: IndicatorPosition = .bottom { + didSet { setNeedsLayout() } + } + + public var minWidth: CGFloat = 44.0 { + didSet { setNeedsLayout() } + } + + public var onTabChange: ((Int) -> Void)? + + public var overflow: Overflow = .none { + didSet { setNeedsLayout() } + } + + public var selectedIndex: Int = 0 { + didSet { setNeedsLayout() } + } + + public var size: Size = .medium { + didSet { setNeedsLayout() } + } + + public var sticky: Bool = false { + didSet { setNeedsLayout() } + } + + public var width: Width = .percentage(0.25) { + didSet { setNeedsLayout() } + } + + 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) + commonInit() + } + + public convenience required init() { + self.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + 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 + } + + public func updateTabItems(with models: [TabItemModel]) { + // 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 + tabItem.textStyle = size.textStyle + 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) + } + + setNeedsLayout() + } + + @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 layoutSubviews() { + super.layoutSubviews() + + // Update tab appearance based on properties + + for (index, tabItem) in tabItems.enumerated() { + if selectedIndex == index { + // Apply selected style to the current tab + if orientation == .vertical { + tabItem.indicatorPosition = .left + } else { + if indicatorPosition == .top { + tabItem.indicatorPosition = .top + } else { + tabItem.indicatorPosition = .bottom + } + } + tabItem.selected = true + } else { + // Apply default style to other tabs + tabItem.indicatorPosition = nil + tabItem.selected = false + } + + if orientation == .horizontal { + tabItem.textPosition = .center + } else { + tabItem.textPosition = .left + } + tabItem.surface = surface + } + + // 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: bounds.width - 1, y: 0, width: 1, height: bounds.height) + } + + layer.addSublayer(borderLineLayer) + } + + // Apply fill container + tabStackView.alignment = fillContainer && orientation == .horizontal ? .fill : .leading + + // Apply indicator fill tab + if orientation == .vertical { + if indicatorFillTab { + tabItems.forEach { $0.label.textAlignment = .left } + } else { + tabItems.forEach { $0.label.textAlignment = .center } + } + } + + // Apply indicator position + if orientation == .horizontal { + if indicatorPosition == .top { + tabStackView.alignment = .top + } else if indicatorPosition == .bottom { + tabStackView.alignment = .bottom + } + } + + // Apply sticky + if sticky && orientation == .vertical { + scrollView.pinTop() + } else { + scrollView.pinTop(layoutMargins.top) + } + + // 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 + } + } + + // 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) + } +} + +public class TabItem: View { + 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() } } + public var textStyle: TextStyle = .bodyMedium + public var textPosition: TextPosition = .center { didSet { setNeedsUpdate() } } + public var indicatorPosition: UIRectEdge? { didSet { setNeedsUpdate() } } + private var labelMinWidthConstraint: NSLayoutConstraint? + private var labelWidthConstraint: NSLayoutConstraint? + + private var textColorConfiguration: SurfaceColorConfiguration { selected ? textColorSelectedConfiguration : textColorNonSelectedConfiguration } + private var textColorNonSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOnlight) + private var textColorSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + + public override init(frame: CGRect) { + super.init(frame: frame) + addSubview(label) + backgroundColor = .clear + label.backgroundColor = .clear + + label.translatesAutoresizingMaskIntoConstraints = false + label + .pinTop(5) + .pinLeading(5) + .pinTrailing(5) + .pinBottom(6) + 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() { + label.textPosition = textPosition + label.text = text + label.textStyle = 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 let indicatorPosition { + addBorder(side: indicatorPosition, width: 4.0, color: .red) + } else { + removeBorders() + } + } +} +