From af82966bade46e9967671bf8a84579b1718ddbb8 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 May 2023 14:48:46 -0500 Subject: [PATCH 01/27] Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tab.swift | 8 ++++++++ VDS/Components/Tabs/Tabs.swift | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 VDS/Components/Tabs/Tab.swift create mode 100644 VDS/Components/Tabs/Tabs.swift diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift new file mode 100644 index 00000000..f713777f --- /dev/null +++ b/VDS/Components/Tabs/Tab.swift @@ -0,0 +1,8 @@ +// +// Tab.swift +// VDS +// +// Created by Matt Bruce on 5/18/23. +// + +import Foundation diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift new file mode 100644 index 00000000..7c503f30 --- /dev/null +++ b/VDS/Components/Tabs/Tabs.swift @@ -0,0 +1,8 @@ +// +// Tabs.swift +// VDS +// +// Created by Matt Bruce on 5/18/23. +// + +import Foundation From 3625db96bf5205c352da3b82d1c6623f220024b0 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 May 2023 14:49:01 -0500 Subject: [PATCH 02/27] added more view extensions Signed-off-by: Matt Bruce --- VDS/Extensions/UIView.swift | 58 ++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/VDS/Extensions/UIView.swift b/VDS/Extensions/UIView.swift index 5b5c909b..011380cb 100644 --- a/VDS/Extensions/UIView.swift +++ b/VDS/Extensions/UIView.swift @@ -176,10 +176,66 @@ extension UIView { extension CALayer { func remove(layerName: String) { - sublayers?.forEach({ layer in + guard let sublayers = sublayers else { + return + } + + sublayers.forEach({ layer in if layer.name?.hasPrefix(layerName) ?? false { layer.removeFromSuperlayer() } }) } } + + +extension UIView { + + public func addBorder(side: UIRectEdge, width: CGFloat, color: UIColor) { + let layerName = borderLayerName(for: side) + layer.remove(layerName: layerName) + + let borderLayer = CALayer() + borderLayer.backgroundColor = color.cgColor + borderLayer.name = layerName + + switch side { + case .left: + borderLayer.frame = CGRect(x: 0, y: 0, width: width, height: frame.height) + case .right: + borderLayer.frame = CGRect(x: frame.width - width, y: 0, width: width, height: frame.height) + case .top: + borderLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: width) + case .bottom: + borderLayer.frame = CGRect(x: 0, y: frame.height - width, width: frame.width, height: width) + default: + break + } + + layer.addSublayer(borderLayer) + } + + public func removeBorders() { + layer.borderWidth = 0 + layer.borderColor = nil + layer.remove(layerName: borderLayerName(for: .top)) + layer.remove(layerName: borderLayerName(for: .left)) + layer.remove(layerName: borderLayerName(for: .right)) + layer.remove(layerName: borderLayerName(for: .bottom)) + } + + private func borderLayerName(for side: UIRectEdge) -> String { + switch side { + case .left: + return "leftBorderLayer" + case .right: + return "rightBorderLayer" + case .top: + return "topBorderLayer" + case .bottom: + return "bottomBorderLayer" + default: + return "" + } + } +} From e4efd296bb15913eec4f7d4016cea7d26bd233d4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 May 2023 14:49:13 -0500 Subject: [PATCH 03/27] adding tabs initial classes Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 16 ++ VDS/Components/Tabs/Tab.swift | 423 +++++++++++++++++++++++++++++++++ VDS/Components/Tabs/Tabs.swift | 346 +++++++++++++++++++++++++++ 3 files changed, 785 insertions(+) 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() + } + } +} + From 18eec4241bebc79309773c89fc2897248ec6f7ac Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 May 2023 15:48:49 -0500 Subject: [PATCH 04/27] more refactoring Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 217 ++++++++++++++------------------- 1 file changed, 89 insertions(+), 128 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 761c41d9..2febfc1b 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -9,7 +9,7 @@ import Foundation import UIKit import VDSColorTokens -public struct TabItemModel { +public struct TabModel { public var text: String public var onClick: (() -> Void)? public var width: CGFloat? @@ -55,52 +55,20 @@ public class Tabs: View { 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() } - } - + 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! @@ -109,7 +77,6 @@ public class Tabs: View { public override init(frame: CGRect) { super.init(frame: frame) - commonInit() } public convenience required init() { @@ -118,10 +85,10 @@ public class Tabs: View { public required init?(coder: NSCoder) { super.init(coder: coder) - commonInit() } - private func commonInit() { + open override func setup() { + super.setup() scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.showsHorizontalScrollIndicator = false @@ -145,7 +112,7 @@ public class Tabs: View { contentView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true } - public func updateTabItems(with models: [TabItemModel]) { + private func updateTabItems(with models: [TabModel]) { // Clear existing tab items for tabItem in tabItems { tabItem.removeFromSuperview() @@ -158,7 +125,6 @@ public class Tabs: View { tabItem.text = model.text tabItem.onClick = model.onClick tabItem.width = model.width - tabItem.textStyle = size.textStyle tabItems.append(tabItem) tabStackView.addArrangedSubview(tabItem) @@ -167,8 +133,7 @@ public class Tabs: View { tabItem.isUserInteractionEnabled = true tabItem.addGestureRecognizer(tapGesture) } - - setNeedsLayout() + setNeedsUpdate() } @objc private func tabItemTapped(_ gesture: UITapGestureRecognizer) { @@ -180,41 +145,38 @@ public class Tabs: View { } } + 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() - // 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") + // 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" @@ -228,35 +190,7 @@ public class Tabs: View { 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 { @@ -265,38 +199,41 @@ public class Tabs: View { 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 + // 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() } } - 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 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) @@ -307,9 +244,10 @@ public class TabItem: View { label.translatesAutoresizingMaskIntoConstraints = false label .pinTop(5) - .pinLeading(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) @@ -331,9 +269,13 @@ public class TabItem: View { } public override func updateView() { - label.textPosition = textPosition + if orientation == .horizontal { + label.textPosition = .center + } else { + label.textPosition = .left + } label.text = text - label.textStyle = textStyle + label.textStyle = size.textStyle label.textColor = textColorConfiguration.getColor(self) if let width { labelMinWidthConstraint?.isActive = false @@ -344,11 +286,30 @@ public class TabItem: View { labelMinWidthConstraint?.isActive = true } - if let indicatorPosition { - addBorder(side: indicatorPosition, width: 4.0, color: .red) + 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) +// } + } From d90045918b0e8c27db1096a941b0b9863349b8ac Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 May 2023 16:01:37 -0500 Subject: [PATCH 05/27] added properties to push down Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 69 +++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 2febfc1b..d98eb9e6 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -30,6 +30,14 @@ public class Tabs: View { public enum IndicatorPosition { case top case bottom + + var value: UIRectEdge { + if self == .top { + return .top + } else { + return .bottom + } + } } public enum Overflow { @@ -56,7 +64,7 @@ public class Tabs: View { } public var onTabChange: ((Int) -> Void)? - public var orientation: Orientation = .vertical { didSet { setNeedsUpdate() } } + public var orientation: Orientation = .horizontal { didSet { setNeedsUpdate() } } public var borderLine: Bool = true { didSet { setNeedsUpdate() } } public var fillContainer: Bool = false { didSet { setNeedsUpdate() } } public var indicatorFillTab: Bool = false { didSet { setNeedsUpdate() } } @@ -175,6 +183,7 @@ public class Tabs: View { tabItem.size = size tabItem.orientation = orientation tabItem.surface = surface + tabItem.indicatorPosition = indicatorPosition } if borderLine { @@ -220,6 +229,7 @@ public class Tabs: View { public class TabItem: View { 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 label: Label = Label() public var onClick: (() -> Void)? { didSet { setNeedsUpdate() } } public var width: CGFloat? { didSet { setNeedsUpdate() } } @@ -228,7 +238,9 @@ public class TabItem: View { private var labelMinWidthConstraint: NSLayoutConstraint? private var labelWidthConstraint: NSLayoutConstraint? private var labelLeadingConstraint: NSLayoutConstraint? - + private var labelTopConstraint: NSLayoutConstraint? + private var labelBottomConstraint: NSLayoutConstraint? + private var textColorConfiguration: SurfaceColorConfiguration { selected ? textColorSelectedConfiguration : textColorNonSelectedConfiguration } private var textColorNonSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOnlight) private var textColorSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) @@ -242,14 +254,20 @@ public class TabItem: View { label.backgroundColor = .clear label.translatesAutoresizingMaskIntoConstraints = false - label - .pinTop(5) - .pinTrailing(5) - .pinBottom(6) + 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 + 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)) @@ -277,6 +295,12 @@ public class TabItem: View { label.text = text label.textStyle = size.textStyle label.textColor = textColorConfiguration.getColor(self) + setNeedsLayout() + } + + public override func setNeedsLayout() { + super.setNeedsLayout() + if let width { labelMinWidthConstraint?.isActive = false labelWidthConstraint?.constant = width @@ -286,23 +310,32 @@ public class TabItem: View { labelMinWidthConstraint?.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 + if selected { - var indicatorPosition: UIRectEdge = .top - if orientation == .vertical { - indicatorPosition = .left - } else { - if indicatorPosition == .top { - indicatorPosition = .top - } else { - indicatorPosition = .bottom - } + var indicator: UIRectEdge = .left + if orientation == .horizontal { + indicator = indicatorPosition.value } - addBorder(side: indicatorPosition, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self)) + addBorder(side: indicator, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self)) } else { removeBorders() } - - setNeedsDisplay() } // public override func draw(_ rect: CGRect) { From 5e229780f5597b06fea8ccdc67a6428c582ecb4a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 May 2023 16:16:18 -0500 Subject: [PATCH 06/27] updated enums Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index d98eb9e6..090d6e97 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -22,12 +22,12 @@ public struct TabModel { } public class Tabs: View { - public enum Orientation { + public enum Orientation: String, CaseIterable{ case vertical case horizontal } - public enum IndicatorPosition { + public enum IndicatorPosition: String, CaseIterable { case top case bottom @@ -40,12 +40,12 @@ public class Tabs: View { } } - public enum Overflow { + public enum Overflow: String, CaseIterable { case scroll case none } - public enum Size { + public enum Size: String, CaseIterable { case medium case large @@ -194,7 +194,7 @@ public class Tabs: View { 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) + borderLineLayer.frame = CGRect(x: 0, y: 0, width: 1, height: bounds.height) } layer.addSublayer(borderLineLayer) @@ -222,7 +222,7 @@ public class Tabs: View { let contentWidth = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.bounds.height) - //addDebugBorder(color: .blue) + addDebugBorder(color: .blue) } } @@ -338,11 +338,11 @@ public class TabItem: View { } } -// public override func draw(_ rect: CGRect) { -// super.draw(rect) -// -// addDebugBorder(color: .green) -// } + public override func draw(_ rect: CGRect) { + super.draw(rect) + + addDebugBorder(color: .green) + } } From 5231379281bbc5bb8f1f27d3b50c181258f0e622 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 May 2023 16:06:23 -0500 Subject: [PATCH 07/27] refactored into file Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Components/Tabs/TabModel.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 VDS/Components/Tabs/TabModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 828585b1..4e304a5f 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E30522950DDA60082B959 /* TitleLockup.swift */; }; EA5E3058295105A40082B959 /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E3057295105A40082B959 /* Tilelet.swift */; }; EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E305929510F8B0082B959 /* EnumSubset.swift */; }; + EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; }; EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; }; EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; }; EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; }; @@ -172,6 +173,7 @@ EA5E30522950DDA60082B959 /* TitleLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockup.swift; sourceTree = ""; }; EA5E3057295105A40082B959 /* Tilelet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilelet.swift; sourceTree = ""; }; EA5E305929510F8B0082B959 /* EnumSubset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumSubset.swift; sourceTree = ""; }; + EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; @@ -520,6 +522,7 @@ isa = PBXGroup; children = ( EA596ABC2A16B4EC00300C4B /* Tab.swift */, + EA5F86C72A1BD99100BC83E4 /* TabModel.swift */, EA596ABE2A16B4F500300C4B /* Tabs.swift */, ); path = Tabs; @@ -819,6 +822,7 @@ EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, + EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */, EA297A5729FB0A360031ED56 /* AppleGuidlinesTouchable.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, diff --git a/VDS/Components/Tabs/TabModel.swift b/VDS/Components/Tabs/TabModel.swift new file mode 100644 index 00000000..8922edfa --- /dev/null +++ b/VDS/Components/Tabs/TabModel.swift @@ -0,0 +1,20 @@ +// +// TabModel.swift +// VDS +// +// Created by Matt Bruce on 5/22/23. +// + +import Foundation + +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 + } +} From aba5490a103ece0140f7f8379de40094e0e283e4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 May 2023 16:06:35 -0500 Subject: [PATCH 08/27] added tab item Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tab.swift | 549 ++++++++-------------------------- 1 file changed, 129 insertions(+), 420 deletions(-) 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)) + } + } +} From c75e5670dfc5d9177cb15bd8cc8cd3d111a6b208 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 May 2023 16:06:45 -0500 Subject: [PATCH 09/27] updated tabs to work better Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 332 ++++++++++++++------------------- 1 file changed, 139 insertions(+), 193 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 090d6e97..a51e8d40 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -9,19 +9,11 @@ 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 { + + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- public enum Orientation: String, CaseIterable{ case vertical case horizontal @@ -63,26 +55,61 @@ public class Tabs: View { case value(CGFloat) } + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- public var onTabChange: ((Int) -> Void)? - public var orientation: Orientation = .horizontal { didSet { setNeedsUpdate() } } + public var orientation: Orientation = .horizontal { didSet { updateTabItems() } } public var borderLine: Bool = true { didSet { setNeedsUpdate() } } - public var fillContainer: Bool = false { didSet { setNeedsUpdate() } } + public var fillContainer: Bool = false { didSet { updateTabItems() } } 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 overflow: Overflow = .scroll { didSet { updateTabItems() } } 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) } } - + public var tabModels: [TabModel] = [] { didSet { updateTabItems() } } + + //rules for width + private var _width: Width = .percentage(0.25) + public var width: Width { + get { + return _width + } + set { + 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).") + } + } + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- private var tabItems: [TabItem] = [] 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) } @@ -95,6 +122,9 @@ public class Tabs: View { super.init(coder: coder) } + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- open override func setup() { super.setup() scrollView = UIScrollView() @@ -116,10 +146,13 @@ public class Tabs: View { 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 { @@ -136,56 +169,95 @@ public class Tabs: View { 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) + 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) } - - @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) + private func scrollToSelectedIndex(animated: Bool) { + if orientation == .horizontal && self.overflow == .scroll, selectedIndex < tabItems.count { + let selectedTab = tabItems[selectedIndex] + scrollView.scrollRectToVisible(selectedTab.frame, animated: animated) } } - - public override func updateView() { + + open override func updateView() { super.updateView() - if fillContainer { + if orientation == .horizontal && 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.selected = selectedIndex == index tabItem.size = size tabItem.orientation = orientation tabItem.surface = surface tabItem.indicatorPosition = indicatorPosition + tabItem.width = tabWidth(for: tabItem) } + + tabStackView.axis = orientation == .horizontal ? .horizontal : .vertical + tabStackView.alignment = orientation == .horizontal ? .fill : .leading + tabStackView.spacing = orientation == .horizontal ? VDSLayout.Spacing.space6X.value : VDSLayout.Spacing.space4X.value + + // 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) + } + } 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" @@ -199,150 +271,24 @@ public class Tabs: View { 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) + 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 + } + } - -public class TabItem: View { - 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 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 labelTopConstraint: NSLayoutConstraint? - private var labelBottomConstraint: 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.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 - - 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) - setNeedsLayout() - } - - public override func setNeedsLayout() { - super.setNeedsLayout() - - if let width { - labelMinWidthConstraint?.isActive = false - labelWidthConstraint?.constant = width - labelWidthConstraint?.isActive = true - } else { - labelWidthConstraint?.isActive = false - labelMinWidthConstraint?.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 - - if selected { - var indicator: UIRectEdge = .left - if orientation == .horizontal { - indicator = indicatorPosition.value - } - addBorder(side: indicator, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self)) - } else { - removeBorders() - } - } - - public override func draw(_ rect: CGRect) { - super.draw(rect) - - addDebugBorder(color: .green) - } - -} - From b88810089725dc89840b9feb31eca9087b4740de Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 23 May 2023 12:20:36 -0500 Subject: [PATCH 10/27] CXTDT-412383 - Badge Corner Radius also updated spacing to tokens Signed-off-by: Matt Bruce --- VDS/Components/Badge/Badge.swift | 7 +++++-- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 VDS/SupportingFiles/ReleaseNotes.txt diff --git a/VDS/Components/Badge/Badge.swift b/VDS/Components/Badge/Badge.swift index c7fed36f..58562fd5 100644 --- a/VDS/Components/Badge/Badge.swift +++ b/VDS/Components/Badge/Badge.swift @@ -54,10 +54,13 @@ open class Badge: View { super.setup() accessibilityElements = [label] - layer.cornerRadius = VDSFormControls.borderradius + layer.cornerRadius = 2 addSubview(label) - label.pinToSuperView(.init(top: 2, left: 4, bottom: 2, right: 4)) + label.pinToSuperView(.init(top: 2, + left: VDSLayout.Spacing.space1X.value, + bottom: 2, + right: VDSLayout.Spacing.space1X.value)) maxWidthConstraint = label.widthAnchor.constraint(lessThanOrEqualToConstant: 100) minWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 23) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -0,0 +1 @@ + From 78ec1acd40ae7ebf8728e3c96c22fa7304ef6a56 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 23 May 2023 12:30:20 -0500 Subject: [PATCH 11/27] initial release notes Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 8 ++++++-- VDS/SupportingFiles/ReleaseNotes.txt | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 4e304a5f..b28a9e70 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ EA5E3058295105A40082B959 /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E3057295105A40082B959 /* Tilelet.swift */; }; EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E305929510F8B0082B959 /* EnumSubset.swift */; }; EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; }; + EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; }; EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; }; EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; }; EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; }; @@ -174,6 +175,7 @@ EA5E3057295105A40082B959 /* Tilelet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilelet.swift; sourceTree = ""; }; EA5E305929510F8B0082B959 /* EnumSubset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumSubset.swift; sourceTree = ""; }; EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; + EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = ""; }; EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; @@ -485,6 +487,7 @@ children = ( EA3361FF2891E14C0071C351 /* Fonts */, EAA5EEB828ECD24B003B3210 /* Icons.xcassets */, + EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */, ); path = SupportingFiles; sourceTree = ""; @@ -795,6 +798,7 @@ buildActionMask = 2147483647; files = ( EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */, + EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */, EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */, EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */, EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */, @@ -1078,7 +1082,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.18; PRODUCT_BUNDLE_IDENTIFIER = com.vzw.vds; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1111,7 +1115,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.18; PRODUCT_BUNDLE_IDENTIFIER = com.vzw.vds; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 8b137891..d0252212 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1 +1,10 @@ +1.17 +======= +- Color Tokens Update +- Changes to Badge Colors +1.18 +======= +- CXTDT-412383 - Badge Corner Radius / Color issue (resolved in previous build) +- Completed Tabs +- Started ButtonIcon From 8d4862ac6a748961d51ec410d3f6d320efa6ecac Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 23 May 2023 12:30:31 -0500 Subject: [PATCH 12/27] updated release notes so far Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index d0252212..19a8ba7e 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,10 +1,14 @@ -1.17 -======= -- Color Tokens Update -- Changes to Badge Colors - 1.18 ======= - CXTDT-412383 - Badge Corner Radius / Color issue (resolved in previous build) - Completed Tabs - Started ButtonIcon + +1.17 +======= +- Color Tokens Update +- Changes to Badge Colors +- updated trailing tooltip label +- updated entry field colors, tooltip +- update toggle size +- added touchable protocol to button From bf13fff473c6cec1c278feb4ca11e05332c0b5f6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 23 May 2023 14:44:52 -0500 Subject: [PATCH 13/27] added fix for typogprahy issue in Tabs Spec Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index a51e8d40..0888feea 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -45,7 +45,10 @@ public class Tabs: View { if self == .medium { return .boldBodyLarge } else { - return .boldTitleSmall + //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 } } } From 814c71fbe7a208215f918d881e55f3ab4b45c0d7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 23 May 2023 14:45:05 -0500 Subject: [PATCH 14/27] debug extension for TextStyle Signed-off-by: Matt Bruce --- VDS/Typography/Typography.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/VDS/Typography/Typography.swift b/VDS/Typography/Typography.swift index a680ebc0..0df38e84 100644 --- a/VDS/Typography/Typography.swift +++ b/VDS/Typography/Typography.swift @@ -329,3 +329,9 @@ extension TextStyle { } } + +extension TextStyle: CustomDebugStringConvertible { + public var debugDescription: String { + "Name: \(self.rawValue) FontFace: \(font.fontName) FontWeight: \(self.rawValue.hasPrefix("bold") ? "bold" : "normal") PointSize: \(font.pointSize) LetterSpacing: \(letterSpacing) LineHeight: \(lineHeight)" + } +} From 38aff5a6372e602b7afd9cc887efe960e79eb01f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 24 May 2023 17:38:28 -0500 Subject: [PATCH 15/27] fixed issue in control configuration Signed-off-by: Matt Bruce --- VDS/Classes/ColorConfiguration.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/VDS/Classes/ColorConfiguration.swift b/VDS/Classes/ColorConfiguration.swift index 9c7c25bd..c52a793f 100644 --- a/VDS/Classes/ColorConfiguration.swift +++ b/VDS/Classes/ColorConfiguration.swift @@ -81,7 +81,7 @@ public class ControlColorConfiguration: KeyColorConfigurable { public typealias KeyType = UIControl.State public typealias ObjectType = Surfaceable & UIControl public var keyColors: [KeyColorConfiguration] = [] - + private var lastKeyColor: KeyColorConfiguration? public required init() { } public func setSurfaceColors(_ lightColor: UIColor, _ darkColor: UIColor, forState state: KeyType) { @@ -94,16 +94,23 @@ public class ControlColorConfiguration: KeyColorConfigurable { // find the exact match if let keyColor = keyColors.first(where: {$0.key == state }) { + lastKeyColor = keyColor return keyColor.surfaceConfig.getColor(surface) } else if state.contains(.disabled), let keyColor = keyColors.first(where: {$0.key == .disabled }) { + lastKeyColor = keyColor return keyColor.surfaceConfig.getColor(surface) } else if state.contains(.highlighted), let keyColor = keyColors.first(where: {$0.key == .highlighted }) { + lastKeyColor = keyColor return keyColor.surfaceConfig.getColor(surface) } else { - return .clear + if let lastKeyColor { + return lastKeyColor.surfaceConfig.getColor(surface) + } else { + return .clear + } } } } From 1205fc7fadd6d96aba1e25f77ababbce0bb28552 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 24 May 2023 17:39:15 -0500 Subject: [PATCH 16/27] moved underneath tabs class Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tab.swift | 258 ++++++++++++++++------------- VDS/Components/Tabs/TabModel.swift | 26 ++- 2 files changed, 162 insertions(+), 122 deletions(-) diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index 2190202c..bd755252 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -9,132 +9,164 @@ import Foundation import VDSColorTokens import Combine -public class TabItem: View { +extension Tabs { - //-------------------------------------------------- - // 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 + open class TabItem: View { + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + ///position of the tab + open var index: Int = 0 - label.translatesAutoresizingMaskIntoConstraints = false - label.pinTrailing() + ///label to write out the text + open var label: Label = Label() - labelTopConstraint = label.topAnchor.constraint(equalTo: topAnchor, constant: 0) - labelTopConstraint?.isActive = true + ///orientation of the tabs + open var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } } - labelBottomConstraint = label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0) - labelBottomConstraint?.isActive = true + ///Size for tab + open var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } } - labelLeadingConstraint = label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0) - labelLeadingConstraint?.isActive = true + ///Text position left or center + open var textPosition: TextPosition = .left { didSet { setNeedsUpdate() } } - labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) - labelWidthConstraint?.isActive = true + ///Sets the Position of the Selected/Hover Border Accent for All Tabs. + open var indicatorPosition: Tabs.IndicatorPosition = .bottom { didSet { setNeedsUpdate() } } - publisher(for: UITapGestureRecognizer()) - .sink { [weak self] _ in - self?.onClick?() - }.store(in: &subscribers) + ///An optional callback that is called when this Tab is clicked. Passes parameters (tabIndex). + open var onClick: ((Int) -> Void)? { didSet { setNeedsUpdate() } } - } - - public override func updateView() { - if orientation == .horizontal { - label.textPosition = .center - } else { - label.textPosition = .left + ///If provided, it will set fixed width for this Tab. + open var width: CGFloat? { didSet { setNeedsUpdate() } } + + ///If provided, it will set this Tab to the Active Tab on render. + open var selected: Bool = false { didSet { setNeedsUpdate() } } + + ///The text label of the tab. + open var text: String? { didSet { setNeedsUpdate() } } + + ///Minimum width for the tab + open var minWidth: CGFloat = 44.0 { 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) } - 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) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - 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 + required public convenience init() { + self.init(frame: .zero) + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func setup() { + super.setup() + addSubview(label) + backgroundColor = .clear + label.backgroundColor = .clear + + label.translatesAutoresizingMaskIntoConstraints = false + label.pinTrailing() + + labelTopConstraint = label.topAnchor.constraint(equalTo: topAnchor) + labelTopConstraint?.isActive = true + + labelBottomConstraint = label.bottomAnchor.constraint(equalTo: bottomAnchor) + labelBottomConstraint?.isActive = true + + labelLeadingConstraint = label.leadingAnchor.constraint(equalTo: leadingAnchor) + labelLeadingConstraint?.isActive = true + + let layoutGuide = UILayoutGuide() + addLayoutGuide(layoutGuide) + + + labelWidthConstraint = layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) + labelWidthConstraint?.isActive = true + + //activate the constraints + NSLayoutConstraint.activate([layoutGuide.topAnchor.constraint(equalTo: topAnchor), + layoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor), + layoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), + layoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor)]) + + publisher(for: UITapGestureRecognizer()) + .sink { [weak self] _ in + guard let self else { return } + self.onClick?(self.index) + }.store(in: &subscribers) + + } + + open override func updateView() { + label.text = text + label.textPosition = textPosition + 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() + } + + open 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), offset: 1) } - addBorder(side: indicator, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self)) } } } diff --git a/VDS/Components/Tabs/TabModel.swift b/VDS/Components/Tabs/TabModel.swift index 8922edfa..02f5bd43 100644 --- a/VDS/Components/Tabs/TabModel.swift +++ b/VDS/Components/Tabs/TabModel.swift @@ -7,14 +7,22 @@ import Foundation -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 +extension Tabs { + public struct TabModel { + + ///Text that goes in the Tab + public var text: String + + ///Click event when you click on a tab + public var onClick: ((Int) -> Void)? + + ///Width of the tab + public var width: CGFloat? + + public init(text: String, onClick: ((Int) -> Void)? = nil, width: CGFloat? = nil) { + self.text = text + self.onClick = onClick + self.width = width + } } } From 4c9c07d75f3ddba0e9d0baf1b43208c2e0ab2439 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 24 May 2023 17:39:34 -0500 Subject: [PATCH 17/27] fixed issues with using only mobile version Signed-off-by: Matt Bruce --- VDS/Components/Tooltip/TooltipAlertViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VDS/Components/Tooltip/TooltipAlertViewController.swift b/VDS/Components/Tooltip/TooltipAlertViewController.swift index b289c118..5de31cd0 100644 --- a/VDS/Components/Tooltip/TooltipAlertViewController.swift +++ b/VDS/Components/Tooltip/TooltipAlertViewController.swift @@ -47,7 +47,8 @@ open class TooltipAlertViewController: UIViewController, Surfaceable { open var surface: Surface = .light { didSet { updateView() }} open var titleText: String = "" { didSet { updateView() }} open var titleLabel = Label().with { label in - label.textStyle = .boldTitleMedium + //use the same font/pointsize for both Title upsizes font in iPad + label.textStyle = UIDevice.isIPad ? .boldTitleSmall : .boldTitleMedium } open var contentText: String = "" { didSet { updateView() }} From a1d25b0dfc3e29fe86979e6cce28373f6b8b9452 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 24 May 2023 17:39:53 -0500 Subject: [PATCH 18/27] updated to place border with an offset Signed-off-by: Matt Bruce --- VDS/Extensions/UIView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VDS/Extensions/UIView.swift b/VDS/Extensions/UIView.swift index 011380cb..327849fc 100644 --- a/VDS/Extensions/UIView.swift +++ b/VDS/Extensions/UIView.swift @@ -191,7 +191,7 @@ extension CALayer { extension UIView { - public func addBorder(side: UIRectEdge, width: CGFloat, color: UIColor) { + public func addBorder(side: UIRectEdge, width: CGFloat, color: UIColor, offset: CGFloat = 0) { let layerName = borderLayerName(for: side) layer.remove(layerName: layerName) @@ -203,11 +203,11 @@ extension UIView { case .left: borderLayer.frame = CGRect(x: 0, y: 0, width: width, height: frame.height) case .right: - borderLayer.frame = CGRect(x: frame.width - width, y: 0, width: width, height: frame.height) + borderLayer.frame = CGRect(x: frame.width - width - offset, y: 0, width: width, height: frame.height) case .top: borderLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: width) case .bottom: - borderLayer.frame = CGRect(x: 0, y: frame.height - width, width: frame.width, height: width) + borderLayer.frame = CGRect(x: 0, y: frame.height - width - offset, width: frame.width, height: width) default: break } From 7fa75eb519b0fce84edfa64ae93d1bc46c1e189e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 24 May 2023 17:40:34 -0500 Subject: [PATCH 19/27] commented out stuff dealing with width since this doesn't apply to this view for now Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 176 +++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 72 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 0888feea..8170a292 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -9,7 +9,7 @@ import Foundation import UIKit import VDSColorTokens -public class Tabs: View { +open class Tabs: View { //-------------------------------------------------- // MARK: - Enums @@ -61,49 +61,77 @@ public class Tabs: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - public var onTabChange: ((Int) -> Void)? - public var orientation: Orientation = .horizontal { didSet { updateTabItems() } } - public var borderLine: Bool = true { didSet { setNeedsUpdate() } } - public var fillContainer: Bool = false { didSet { updateTabItems() } } - 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 { updateTabItems() } } - public var selectedIndex: Int = 0 { didSet { setNeedsUpdate() } } - public var size: Size = .medium { didSet { setNeedsUpdate() } } - public var sticky: Bool = false { didSet { setNeedsUpdate() } } - public var tabModels: [TabModel] = [] { didSet { updateTabItems() } } + ///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() } } - //rules for width - private var _width: Width = .percentage(0.25) - public var width: Width { - get { - return _width - } - set { - 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).") - } - } - } - } + ///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 tabItems: [TabItem] = [] private var tabStackView: UIStackView! private var scrollView: UIScrollView! private var contentView: View! @@ -162,7 +190,6 @@ public class Tabs: View { tabItem.removeFromSuperview() } tabItems.removeAll() - // Create new tab items from the models for model in models { let tabItem = TabItem() @@ -195,38 +222,43 @@ public class Tabs: View { open override func updateView() { super.updateView() + + // Update the stackview properties if orientation == .horizontal && fillContainer { tabStackView.distribution = .fillEqually } else { - tabStackView.distribution = .fill - } - - // Update tab appearance based on properties - for (index, tabItem) in tabItems.enumerated() { - tabItem.selected = selectedIndex == index - tabItem.size = size - tabItem.orientation = orientation - tabItem.surface = surface - tabItem.indicatorPosition = indicatorPosition - tabItem.width = tabWidth(for: tabItem) + 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) - } +// 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 { @@ -278,20 +310,20 @@ public class Tabs: View { 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 - } +// //-------------------------------------------------- +// // 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 +// } } From bf63fe9e6141913a97a5c44ab2a022b2401b7c0e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 07:58:08 -0500 Subject: [PATCH 20/27] updated tab to set with in vertical Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tab.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index bd755252..c8e80b19 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -128,7 +128,7 @@ extension Tabs { label.textStyle = size.textStyle label.textColor = textColorConfiguration.getColor(self) labelWidthConstraint?.isActive = false - if let width { + if let width, orientation == .vertical { labelWidthConstraint = label.widthAnchor.constraint(equalToConstant: width) } else { labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) From fb8ccd6d10a8971ee8d907a08a9c5cb84aca6950 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 07:58:25 -0500 Subject: [PATCH 21/27] removed old code added tabSpace readonly Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 58 +++------------------------------- 1 file changed, 4 insertions(+), 54 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 8170a292..a362ae4f 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -61,7 +61,8 @@ open class Tabs: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - ///An optional callback that is called when the selectedIndex changes. Passes parameters (event, tabIndex). + ///An + /// 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 @@ -97,36 +98,8 @@ open class Tabs: View { ///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 tabSpacing: CGFloat { tabStackView.spacing } + open var tabItems: [TabItem] = [] //-------------------------------------------------- @@ -252,12 +225,6 @@ open class Tabs: View { // 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 @@ -309,21 +276,4 @@ open class Tabs: View { 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 -// } - } From 62ffdebbfc2bbd30e1f52204440cab8fef77d598 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 08:03:31 -0500 Subject: [PATCH 22/27] added tabs container Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Components/Tabs/TabsContainer.swift | 223 ++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 VDS/Components/Tabs/TabsContainer.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index b28a9e70..0e9101e3 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E305929510F8B0082B959 /* EnumSubset.swift */; }; EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; }; EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; }; + EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */; }; EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; }; EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; }; EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; }; @@ -176,6 +177,7 @@ EA5E305929510F8B0082B959 /* EnumSubset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumSubset.swift; sourceTree = ""; }; EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = ""; }; + EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = ""; }; EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; @@ -527,6 +529,7 @@ EA596ABC2A16B4EC00300C4B /* Tab.swift */, EA5F86C72A1BD99100BC83E4 /* TabModel.swift */, EA596ABE2A16B4F500300C4B /* Tabs.swift */, + EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */, ); path = Tabs; sourceTree = ""; @@ -864,6 +867,7 @@ EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */, EA33624728931B050071C351 /* Initable.swift in Sources */, EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */, + EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */, EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */, EAC9258F2911C9DE00091998 /* EntryField.swift in Sources */, EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */, diff --git a/VDS/Components/Tabs/TabsContainer.swift b/VDS/Components/Tabs/TabsContainer.swift new file mode 100644 index 00000000..3b7cf588 --- /dev/null +++ b/VDS/Components/Tabs/TabsContainer.swift @@ -0,0 +1,223 @@ +// +// TabsContainer.swift +// VDS +// +// Created by Matt Bruce on 5/25/23. +// + +import Foundation +import UIKit + +open class TabsContainer: View { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var tabMenu = Tabs() + + ///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: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } } + + ///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 { setNeedsUpdate() } } + + ///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: Tabs.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: Tabs.Overflow = .scroll { didSet { setNeedsUpdate() } } + + ///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: Tabs.Size = .medium { didSet { setNeedsUpdate() } } + + ///Space between the Tabs and Contentl. + open var space: CGFloat = 5.0 { didSet { setNeedsUpdate() } } + + ///When true, Tabs will be sticky to top of page, when orientation is vertical. + open var sticky: Bool = false { didSet { setNeedsUpdate() } } + + ///rules for width + private var _width: Tabs.Width = .percentage(0.25) + + ///Width of all Tabs when orientation is vertical, defaults to 25%. + open var width: Tabs.Width { + get { + return _width + } + set { + 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).") + } + } + } + } + + ///Model of the Tabs you are wanting to show. + open var tabModels: [TabModel] = [] { + didSet { + tabMenu.tabModels = tabModels.compactMap{ $0.model } + contentView.arrangedSubviews.forEach{ $0.removeFromSuperview() } + tabModels.forEach { + let view = $0.view + view.isHidden = true + contentView.addArrangedSubview(view) + } + setNeedsUpdate() + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var contentViewWidthConstraint: NSLayoutConstraint? + + private var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.alignment = .fill + $0.distribution = .fill + } + + private var contentView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.alignment = .fill + $0.distribution = .fillProportionally + $0.axis = .vertical + $0.spacing = 10 + } + + private var tabMenuLayoutGuide = UILayoutGuide() + + open override func setup() { + super.setup() + + tabMenu.addLayoutGuide(tabMenuLayoutGuide) + addSubview(stackView) + stackView.pinToSuperView() + stackView.addArrangedSubview(tabMenu) + stackView.addArrangedSubview(contentView) + + NSLayoutConstraint.activate([ + tabMenuLayoutGuide.topAnchor.constraint(equalTo: topAnchor), + tabMenuLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor), + tabMenuLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), + tabMenuLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) + } + + open override func updateView() { + super.updateView() + + stackView.alignment = orientation == .horizontal ? .fill : .top + stackView.axis = orientation == .horizontal ? .vertical : .horizontal + stackView.spacing = space + + tabMenu.onTabChange = { [weak self] index in + guard let self else { return } + self.tabClicked(index: index) + } + + contentViewWidthConstraint?.isActive = false + + if orientation == .vertical { + switch width { + case .percentage(let amount): + contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalTo: tabMenuLayoutGuide.widthAnchor, multiplier: amount) + case .value(let amount): + contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalToConstant: amount) + } + contentViewWidthConstraint?.isActive = true + } + + tabMenu.surface = surface + tabMenu.disabled = disabled + tabMenu.orientation = orientation + tabMenu.borderLine = borderLine + tabMenu.fillContainer = fillContainer + tabMenu.indicatorFillTab = indicatorFillTab + tabMenu.indicatorPosition = indicatorPosition + tabMenu.minWidth = minWidth + tabMenu.overflow = overflow + tabMenu.selectedIndex = selectedIndex + tabMenu.size = size + tabMenu.sticky = sticky + tabMenu.tabItems.forEach { + $0.width = tabWidth(for: $0) + } + setSelected(index: selectedIndex) + + tabModels.forEach { + var view = $0.view + view.surface = surface + } + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func tabWidth(for item: Tabs.TabItem) -> CGFloat? { + guard orientation == .vertical else { return item.width } + var calculated: CGFloat + switch width { + case .percentage(let percent): + calculated = (bounds.width * percent) - tabMenu.tabSpacing + case .value(let value): + calculated = value - tabMenu.tabSpacing + } + + return calculated > minWidth ? calculated : minWidth + } + + private func tabClicked(index: Int) { + onTabChange?(index) + setSelected(index: index) + } + + private func setSelected(index: Int) { + for (modelIndex, model) in tabModels.enumerated() { + let view = model.view + let shouldShow = index == modelIndex + view.isHidden = !shouldShow + } + } +} + +extension TabsContainer { + public struct TabModel { + public typealias AnySurfaceableView = UIView & Surfaceable + public var model: Tabs.TabModel + public var view: AnySurfaceableView + + public init(model: Tabs.TabModel, view: AnySurfaceableView) { + self.model = model + self.view = view + } + } +} From a872cf801c1137e1cc4dcd49c18435d1c5e7de27 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 10:25:21 -0500 Subject: [PATCH 23/27] fixed orientation bug Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tabs.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index a362ae4f..111b9347 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -52,12 +52,7 @@ open class Tabs: View { } } } - - public enum Width { - case percentage(CGFloat) - case value(CGFloat) - } - + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -200,7 +195,7 @@ open class Tabs: View { if orientation == .horizontal && fillContainer { tabStackView.distribution = .fillEqually } else { - tabStackView.distribution = .fillProportionally + tabStackView.distribution = orientation == .horizontal ? .fillProportionally : .fill } tabStackView.axis = orientation == .horizontal ? .horizontal : .vertical @@ -217,6 +212,7 @@ open class Tabs: View { tabItem.orientation = orientation tabItem.surface = surface tabItem.indicatorPosition = indicatorPosition + tabItem.accessibilityLabel = "\(tabItem.text) \(tabItem.selected ? "selected" : "unselected") \(index+1) of \(tabItems.count)" } // Deactivate old constraint From 9c0e0cf0e8905a6ffe86f010bc65a68975f2f61f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 10:25:44 -0500 Subject: [PATCH 24/27] add accessibility and constants Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tab.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index c8e80b19..f780d469 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -43,7 +43,7 @@ extension Tabs { open var selected: Bool = false { didSet { setNeedsUpdate() } } ///The text label of the tab. - open var text: String? { didSet { setNeedsUpdate() } } + open var text: String = "Tab" { didSet { setNeedsUpdate() } } ///Minimum width for the tab open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } } @@ -87,6 +87,8 @@ extension Tabs { super.setup() addSubview(label) backgroundColor = .clear + accessibilityTraits = .button + label.backgroundColor = .clear label.translatesAutoresizingMaskIntoConstraints = false @@ -104,8 +106,7 @@ extension Tabs { let layoutGuide = UILayoutGuide() addLayoutGuide(layoutGuide) - - labelWidthConstraint = layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) + labelWidthConstraint = layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth) labelWidthConstraint?.isActive = true //activate the constraints @@ -131,7 +132,7 @@ extension Tabs { if let width, orientation == .vertical { labelWidthConstraint = label.widthAnchor.constraint(equalToConstant: width) } else { - labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0) + labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth) } labelWidthConstraint?.isActive = true From 2f7315db86a08090f880f55d549b45a899f64355 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 13:04:53 -0500 Subject: [PATCH 25/27] refactored enum into container Signed-off-by: Matt Bruce --- VDS/Components/Tabs/TabsContainer.swift | 35 ++++++++++--------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/VDS/Components/Tabs/TabsContainer.swift b/VDS/Components/Tabs/TabsContainer.swift index 3b7cf588..623e5b73 100644 --- a/VDS/Components/Tabs/TabsContainer.swift +++ b/VDS/Components/Tabs/TabsContainer.swift @@ -9,7 +9,14 @@ import Foundation import UIKit open class TabsContainer: View { - + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- + public enum Width { + case percentage(CGFloat) + case value(CGFloat) + } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -52,10 +59,10 @@ open class TabsContainer: View { open var sticky: Bool = false { didSet { setNeedsUpdate() } } ///rules for width - private var _width: Tabs.Width = .percentage(0.25) + private var _width: Width = .percentage(0.25) ///Width of all Tabs when orientation is vertical, defaults to 25%. - open var width: Tabs.Width { + open var width: Width { get { return _width } @@ -144,8 +151,8 @@ open class TabsContainer: View { self.tabClicked(index: index) } + //update tabs width constraints contentViewWidthConstraint?.isActive = false - if orientation == .vertical { switch width { case .percentage(let amount): @@ -153,8 +160,10 @@ open class TabsContainer: View { case .value(let amount): contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalToConstant: amount) } - contentViewWidthConstraint?.isActive = true + } else { + contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalTo: widthAnchor) } + contentViewWidthConstraint?.isActive = true tabMenu.surface = surface tabMenu.disabled = disabled @@ -168,9 +177,6 @@ open class TabsContainer: View { tabMenu.selectedIndex = selectedIndex tabMenu.size = size tabMenu.sticky = sticky - tabMenu.tabItems.forEach { - $0.width = tabWidth(for: $0) - } setSelected(index: selectedIndex) tabModels.forEach { @@ -182,19 +188,6 @@ open class TabsContainer: View { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- - private func tabWidth(for item: Tabs.TabItem) -> CGFloat? { - guard orientation == .vertical else { return item.width } - var calculated: CGFloat - switch width { - case .percentage(let percent): - calculated = (bounds.width * percent) - tabMenu.tabSpacing - case .value(let value): - calculated = value - tabMenu.tabSpacing - } - - return calculated > minWidth ? calculated : minWidth - } - private func tabClicked(index: Int) { onTabChange?(index) setSelected(index: index) From a18d2e2e000a76d735164e0e6d5f7a890713889f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 13:08:46 -0500 Subject: [PATCH 26/27] refactored names for Tabs and related classes Signed-off-by: Matt Bruce --- VDS/Components/Tabs/Tab.swift | 3 ++- VDS/Components/Tabs/Tabs.swift | 23 ++++++++++++----------- VDS/Components/Tabs/TabsContainer.swift | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index f780d469..2b9ff6bc 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -11,7 +11,8 @@ import Combine extension Tabs { - open class TabItem: View { + @objc(VDSTab) + open class Tab: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 111b9347..f886e655 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -9,6 +9,7 @@ import Foundation import UIKit import VDSColorTokens +@objc(VDSTabs) open class Tabs: View { //-------------------------------------------------- @@ -95,7 +96,7 @@ open class Tabs: View { open var tabSpacing: CGFloat { tabStackView.spacing } - open var tabItems: [TabItem] = [] + open var tabViews: [Tab] = [] //-------------------------------------------------- // MARK: - Private Properties @@ -154,24 +155,24 @@ open class Tabs: View { private func updateTabItems(with models: [TabModel]) { // Clear existing tab items - for tabItem in tabItems { + for tabItem in tabViews { tabItem.removeFromSuperview() } - tabItems.removeAll() + tabViews.removeAll() // Create new tab items from the models for model in models { - let tabItem = TabItem() + let tabItem = Tab() tabItem.text = model.text tabItem.onClick = model.onClick tabItem.width = model.width - tabItems.append(tabItem) + tabViews.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) { + guard let self, let tabItem = gesture.view as? Tab else { return } + if let selectedIndex = tabViews.firstIndex(of: tabItem) { self.selectedIndex = selectedIndex self.onTabChange?(selectedIndex) } @@ -182,8 +183,8 @@ open class Tabs: View { } private func scrollToSelectedIndex(animated: Bool) { - if orientation == .horizontal && self.overflow == .scroll, selectedIndex < tabItems.count { - let selectedTab = tabItems[selectedIndex] + if orientation == .horizontal && self.overflow == .scroll, selectedIndex < tabViews.count { + let selectedTab = tabViews[selectedIndex] scrollView.scrollRectToVisible(selectedTab.frame, animated: animated) } } @@ -203,7 +204,7 @@ open class Tabs: View { tabStackView.spacing = orientation == .horizontal ? VDSLayout.Spacing.space6X.value : VDSLayout.Spacing.space4X.value // Update tab appearance based on properties - for (index, tabItem) in tabItems.enumerated() { + for (index, tabItem) in tabViews.enumerated() { tabItem.selected = selectedIndex == index tabItem.index = index tabItem.minWidth = minWidth @@ -212,7 +213,7 @@ open class Tabs: View { tabItem.orientation = orientation tabItem.surface = surface tabItem.indicatorPosition = indicatorPosition - tabItem.accessibilityLabel = "\(tabItem.text) \(tabItem.selected ? "selected" : "unselected") \(index+1) of \(tabItems.count)" + tabItem.accessibilityLabel = "\(tabItem.text) \(tabItem.selected ? "selected" : "unselected") \(index+1) of \(tabViews.count)" } // Deactivate old constraint diff --git a/VDS/Components/Tabs/TabsContainer.swift b/VDS/Components/Tabs/TabsContainer.swift index 623e5b73..ccb01c1c 100644 --- a/VDS/Components/Tabs/TabsContainer.swift +++ b/VDS/Components/Tabs/TabsContainer.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +@objc(VDSTabsContainer) open class TabsContainer: View { //-------------------------------------------------- // MARK: - Enums From bc7e377c87053c4701f23fb33d58819b895967ad Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 May 2023 14:34:04 -0500 Subject: [PATCH 27/27] updated release notes and version Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 8 ++++---- VDS/SupportingFiles/ReleaseNotes.txt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 0e9101e3..2f938f80 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -1070,7 +1070,7 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 18; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1086,7 +1086,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.18; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.vzw.vds; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1103,7 +1103,7 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 18; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1119,7 +1119,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.18; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.vzw.vds; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 19a8ba7e..64ba426f 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,10 +1,10 @@ -1.18 +1.0.18 ======= - CXTDT-412383 - Badge Corner Radius / Color issue (resolved in previous build) - Completed Tabs - Started ButtonIcon -1.17 +1.0.17 ======= - Color Tokens Update - Changes to Badge Colors