diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index ae137257..2190202c 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -7,425 +7,134 @@ 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 -// } -//} +public class TabItem: View { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var label: Label = Label() + public var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } } + public var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } } + public var indicatorPosition: Tabs.IndicatorPosition = .bottom { didSet { setNeedsUpdate() } } + 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() } } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var labelWidthConstraint: NSLayoutConstraint? + private var labelLeadingConstraint: NSLayoutConstraint? + private var labelTopConstraint: NSLayoutConstraint? + private var labelBottomConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + 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 + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public override init(frame: CGRect) { + super.init(frame: frame) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required public convenience init() { + self.init(frame: .zero) + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + override public func setup() { + super.setup() + addSubview(label) + backgroundColor = .clear + label.backgroundColor = .clear + + label.translatesAutoresizingMaskIntoConstraints = false + label.pinTrailing() + + labelTopConstraint = label.topAnchor.constraint(equalTo: topAnchor, constant: 0) + labelTopConstraint?.isActive = true + + labelBottomConstraint = label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0) + labelBottomConstraint?.isActive = true + + labelLeadingConstraint = label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0) + labelLeadingConstraint?.isActive = true + + labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) + labelWidthConstraint?.isActive = true + + publisher(for: UITapGestureRecognizer()) + .sink { [weak self] _ in + self?.onClick?() + }.store(in: &subscribers) + + } + + 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) + + labelWidthConstraint?.isActive = false + if let width { + labelWidthConstraint = label.widthAnchor.constraint(equalToConstant: width) + } else { + labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) + } + labelWidthConstraint?.isActive = true + + var leadingSpace: CGFloat + if orientation == .horizontal { + leadingSpace = 0 + } else { + leadingSpace = size == .medium ? VDSLayout.Spacing.space4X.value : VDSLayout.Spacing.space6X.value + } + labelLeadingConstraint?.constant = leadingSpace + + var otherSpace: CGFloat + if orientation == .horizontal { + otherSpace = size == .medium ? VDSLayout.Spacing.space3X.value : VDSLayout.Spacing.space4X.value + } else { + otherSpace = VDSLayout.Spacing.space2X.value + } + labelTopConstraint?.constant = otherSpace + labelBottomConstraint?.constant = -otherSpace + + setNeedsLayout() + } + + public override func layoutSubviews() { + super.layoutSubviews() + + removeBorders() + + if selected { + var indicator: UIRectEdge = .left + if orientation == .horizontal { + indicator = indicatorPosition.value + } + addBorder(side: indicator, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self)) + } + } +}