From 2e99210dfe6481974b75f6d562028915ea10dda8 Mon Sep 17 00:00:00 2001 From: vasavk Date: Wed, 24 Apr 2024 18:06:21 +0530 Subject: [PATCH 01/87] Digital ACT-191 ONEAPP-7016 story: added new page --- VDS.xcodeproj/project.pbxproj | 12 ++++++ VDS/Components/Calendar/Calendar.swift | 56 ++++++++++++++++++++++++++ VDS/VDS.docc/VDS.md | 1 + 3 files changed, 69 insertions(+) create mode 100644 VDS/Components/Calendar/Calendar.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index f805c10f..35f61ed9 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */; }; 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */; }; 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; + 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; @@ -205,6 +206,7 @@ 186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownSelect.swift; sourceTree = ""; }; 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DropdownSelectChangeLog.txt; sourceTree = ""; }; 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; + 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; @@ -415,6 +417,14 @@ path = DropdownSelect; sourceTree = ""; }; + 18A3F1202BD8F5DE00498E4A /* Calendar */ = { + isa = PBXGroup; + children = ( + 18A3F1292BD9298900498E4A /* Calendar.swift */, + ); + path = Calendar; + sourceTree = ""; + }; 18A65A002B96E7E1006602CC /* Breadcrumbs */ = { isa = PBXGroup; children = ( @@ -573,6 +583,7 @@ EAD062AE2A3B87210015965D /* BadgeIndicator */, 18A65A002B96E7E1006602CC /* Breadcrumbs */, EA0FC2BE2912D18200DF80B4 /* Buttons */, + 18A3F1202BD8F5DE00498E4A /* Calendar */, 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */, EAF7F092289985E200B287F5 /* Checkbox */, 186D13C92BBA8A3500986B53 /* DropdownSelect */, @@ -1106,6 +1117,7 @@ EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */, EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */, EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */, + 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */, 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */, EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, 71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift new file mode 100644 index 00000000..b6aa9ecf --- /dev/null +++ b/VDS/Components/Calendar/Calendar.swift @@ -0,0 +1,56 @@ +// +// Calendar.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 19/04/24. +// + +import Foundation +import UIKit +import VDSTokens +import Combine + +/// A calendar is a monthly view that lets customers select a single date. +@objc(VDSCalendar) +open class CalendarBase: View { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 290, height: 300) } + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + open override func setup() { + super.setup() + isAccessibilityElement = false + } + + open override func reset() { + super.reset() + } +} diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index 607e09f5..d145ea0a 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -25,6 +25,7 @@ Using the system allows designers and developers to collaborate more easily and - ``Button`` - ``ButtonIcon`` - ``ButtonGroup`` +- ``CalendarBase`` - ``CarouselScrollbar`` - ``Checkbox`` - ``CheckboxItem`` From 440f516522551cac8d1942cfc627db5e17cb16a5 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 26 Apr 2024 17:02:18 +0530 Subject: [PATCH 02/87] Digital ACT-191 ONEAPP-7016 story: changes for calendar base view with header and footer. --- VDS.xcodeproj/project.pbxproj | 8 ++ VDS/Components/Calendar/Calendar.swift | 100 +++++++++++++++++- .../CalendarDateCollectionViewCell.swift | 54 ++++++++++ .../Calendar/CalendarHeaderReusableView.swift | 57 ++++++++++ 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 VDS/Components/Calendar/CalendarDateCollectionViewCell.swift create mode 100644 VDS/Components/Calendar/CalendarHeaderReusableView.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 35f61ed9..5a2e0c91 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */; }; 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; + 18A3F1322BD944E800498E4A /* CalendarDateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */; }; + 18A3F1382BDA693000498E4A /* CalendarHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; @@ -207,6 +209,8 @@ 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DropdownSelectChangeLog.txt; sourceTree = ""; }; 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; + 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateCollectionViewCell.swift; sourceTree = ""; }; + 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderReusableView.swift; sourceTree = ""; }; 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; @@ -421,6 +425,8 @@ isa = PBXGroup; children = ( 18A3F1292BD9298900498E4A /* Calendar.swift */, + 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */, + 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */, ); path = Calendar; sourceTree = ""; @@ -1144,6 +1150,7 @@ 71ACE89C2BA0451200FB6ADC /* PaginationContainer.swift in Sources */, EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, + 18A3F1322BD944E800498E4A /* CalendarDateCollectionViewCell.swift in Sources */, EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */, 71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */, @@ -1153,6 +1160,7 @@ 71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */, EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */, + 18A3F1382BDA693000498E4A /* CalendarHeaderReusableView.swift in Sources */, EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */, 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index b6aa9ecf..641aee5d 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -32,12 +32,35 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 290, height: 300) } + internal var containerSize: CGSize { CGSize(width: 320, height: 376) } //width:320/328 + private let cellItemSize = CGSize(width: 40, height: 40) + private let headerHeight = 104.0 + private let footerHeight = 56.0 + private let items = 35 internal var containerView = View().with { $0.clipsToBounds = true } + ///Collectionview to render Breadcrumb Items + private lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collectionView.isScrollEnabled = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.register(CalendarDateCollectionViewCell.self, forCellWithReuseIdentifier: CalendarDateCollectionViewCell.identifier) + collectionView.register(CalendarHeaderReusableView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: CalendarHeaderReusableView.identifier) + collectionView.register(CalendarFooterReusableView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: CalendarFooterReusableView.identifier) + return collectionView + }() + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -48,9 +71,84 @@ open class CalendarBase: View { open override func setup() { super.setup() isAccessibilityElement = false + + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) + .width(containerSize.width) + + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // Calendar View + containerView.addSubview(collectionView) + collectionView.pinToSuperView() } + override open func layoutSubviews() { + super.layoutSubviews() + } + open override func reset() { super.reset() } } + +extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + //-------------------------------------------------- + // MARK: - UICollectionView Delegate & Datasource + //-------------------------------------------------- + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + items + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } + return cell + } + + public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + if kind == UICollectionView.elementKindSectionHeader { + // Header + guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { + return UICollectionReusableView() + } + header.configure(with: true) + return header + } else { + // Footer + if kind == UICollectionView.elementKindSectionFooter { + guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { + return UICollectionReusableView() + } + footer.configure(with: true) + return footer + } + } + return UICollectionReusableView() + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + return CGSize(width: collectionView.frame.size.width, height: headerHeight) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + return CGSize(width: collectionView.frame.size.width, height: footerHeight) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return cellItemSize + } + +} diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift new file mode 100644 index 00000000..156cce04 --- /dev/null +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -0,0 +1,54 @@ +// +// CalendarDateCollectionViewCell.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 24/04/24. +// + +import Foundation +import UIKit +import VDSTokens + +///This is customised view for Calendar cell item +final class CalendarDateCollectionViewCell: UICollectionViewCell { + + ///Identifier for the Calendar Date Cell + static let identifier: String = String(describing: CalendarDateCollectionViewCell.self) + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- +// internal var stackView: UIStackView = { +// return UIStackView().with { +// $0.translatesAutoresizingMaskIntoConstraints = false +// $0.axis = .horizontal +// $0.distribution = .fill +// $0.alignment = .center +// $0.spacing = VDSLayout.space2X +// $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) +// $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) +// } +// }() +// +// private lazy var selectionBackgroundView = View().with { +// $0.translatesAutoresizingMaskIntoConstraints = false +// $0.clipsToBounds = true +// $0.backgroundColor = .systemRed +// } +// +// private lazy var numberLabel = Label().with { +// $0.translatesAutoresizingMaskIntoConstraints = false +// $0.textAlignment = .center +// // $0.font +// // $0.textColor +// } + + override init(frame:CGRect) { + super.init(frame: frame) + contentView.backgroundColor = .link + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/VDS/Components/Calendar/CalendarHeaderReusableView.swift b/VDS/Components/Calendar/CalendarHeaderReusableView.swift new file mode 100644 index 00000000..3f139a10 --- /dev/null +++ b/VDS/Components/Calendar/CalendarHeaderReusableView.swift @@ -0,0 +1,57 @@ +// +// CalendarHeaderReusableView.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 24/04/24. +// + +import UIKit + +/// Custom header view +class CalendarHeaderReusableView: UICollectionReusableView { + + ///Identifier for the Calendar Header Reusable View + static let identifier: String = String(describing: CalendarHeaderReusableView.self) + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with color: Bool) { + // Make a view and make in generic and dynamic + self.backgroundColor = .orange + } + + override func layoutSubviews() { + super.layoutSubviews() + } +} + +/// Custom footer view +class CalendarFooterReusableView: UICollectionReusableView { + + ///Identifier for the Calendar Footer Reusable View + static let identifier: String = String(describing: CalendarFooterReusableView.self) + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with color: Bool) { + // Make a view and make in generic and dynamic + self.backgroundColor = .green + } + + override func layoutSubviews() { + super.layoutSubviews() + } +} + From 2b16fecdc353020b3dadc998913042413072c1e4 Mon Sep 17 00:00:00 2001 From: vasavk Date: Mon, 29 Apr 2024 10:10:26 +0530 Subject: [PATCH 03/87] Digital ACT-191 ONEAPP-7016 story: added calendar indicator model --- VDS.xcodeproj/project.pbxproj | 4 +++ .../Calendar/CalendarIndicatorModel.swift | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 VDS/Components/Calendar/CalendarIndicatorModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 5a2e0c91..1890cb34 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; + 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; @@ -215,6 +216,7 @@ 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; + 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -427,6 +429,7 @@ 18A3F1292BD9298900498E4A /* Calendar.swift */, 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */, 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */, + 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, ); path = Calendar; sourceTree = ""; @@ -1247,6 +1250,7 @@ EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */, EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */, + 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */, EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */, 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, ); diff --git a/VDS/Components/Calendar/CalendarIndicatorModel.swift b/VDS/Components/Calendar/CalendarIndicatorModel.swift new file mode 100644 index 00000000..04ad9714 --- /dev/null +++ b/VDS/Components/Calendar/CalendarIndicatorModel.swift @@ -0,0 +1,25 @@ +// +// CalendarIndicatorModel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 26/04/24. +// + +import Foundation + +/// Custom data type for indicators prop +extension CalendarBase { + public struct CalendarIndicatorModel { + + /// Text that shown to an indicator for legend + public var label: String + + /// Date to an indicator + public var date: Date + + public init(label: String, date: Date) { + self.label = label + self.date = date + } + } +} From 58ceee5d47f8f9f006609a51b1e6a3410679cddf Mon Sep 17 00:00:00 2001 From: vasavk Date: Mon, 29 Apr 2024 10:13:58 +0530 Subject: [PATCH 04/87] Digital ACT-191 ONEAPP-7016 story: added properties --- VDS/Components/Calendar/Calendar.swift | 58 +++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 641aee5d..cb938dde 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -29,6 +29,45 @@ open class CalendarBase: View { super.init(coder: coder) } + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// If set to true, the calendar will not have a border. + open var hideContainerBorder: Bool = false + + /// If set to true, the calendar will not have current date indication. + open var hideCurrentDateIndicator: Bool = false + + /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be inactive + open var activeDates: [Date] = [] + + /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be active. + open var inactiveDates: [Date] = [] + + /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. + open var minDate: Date? + + /// If provided, the calendar will allow a selection to be made up to this date. + open var maxDate: Date? + + /// If provided, this is the date that will show as selected by the Calendar. + /// If no value is provided, the current date will be used. If null is provided, no date will be selected. + open var selectedDate: Date? + + /// If provided, the calendar will be rendered with transparent background. + open var transparentBackground: Bool = false + + /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. + open var indicators: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + + /// Array of indicators for the legend. + open var indicatorData: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + + /// A callback when the selected date changes.. + open var onChangeSelectedDate: ((Date) -> String)? + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -37,12 +76,12 @@ open class CalendarBase: View { private let headerHeight = 104.0 private let footerHeight = 56.0 private let items = 35 - + internal var containerView = View().with { $0.clipsToBounds = true } - ///Collectionview to render Breadcrumb Items + /// Collectionview to load calendar month view private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) collectionView.isScrollEnabled = false @@ -53,11 +92,11 @@ open class CalendarBase: View { collectionView.showsVerticalScrollIndicator = false collectionView.register(CalendarDateCollectionViewCell.self, forCellWithReuseIdentifier: CalendarDateCollectionViewCell.identifier) collectionView.register(CalendarHeaderReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: CalendarHeaderReusableView.identifier) + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: CalendarHeaderReusableView.identifier) collectionView.register(CalendarFooterReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, - withReuseIdentifier: CalendarFooterReusableView.identifier) + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: CalendarFooterReusableView.identifier) return collectionView }() @@ -85,9 +124,9 @@ open class CalendarBase: View { // Calendar View containerView.addSubview(collectionView) - collectionView.pinToSuperView() + collectionView.pinToSuperView() } - + override open func layoutSubviews() { super.layoutSubviews() } @@ -146,9 +185,8 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return VDSLayout.space1X } - + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return cellItemSize } - } From 7438d65fe53d2a46959c26be258eab2945cbf346 Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 30 Apr 2024 11:17:14 +0530 Subject: [PATCH 05/87] Digital ACT-191 ONEAPP-7016 story: updating footer with indicators data --- VDS.xcodeproj/project.pbxproj | 12 +- VDS/Components/Calendar/Calendar.swift | 13 +- .../Calendar/CalendarIndicatorModel.swift | 4 +- .../Calendar/CalendarLegendView.swift | 204 ++++++++++++++++++ ...eView.swift => CalendarReusableView.swift} | 13 +- 5 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 VDS/Components/Calendar/CalendarLegendView.swift rename VDS/Components/Calendar/{CalendarHeaderReusableView.swift => CalendarReusableView.swift} (83%) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 1890cb34..354969af 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -18,12 +18,13 @@ 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; 18A3F1322BD944E800498E4A /* CalendarDateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */; }; - 18A3F1382BDA693000498E4A /* CalendarHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; + 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */; }; + 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; @@ -211,12 +212,13 @@ 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateCollectionViewCell.swift; sourceTree = ""; }; - 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderReusableView.swift; sourceTree = ""; }; 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; + 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarLegendView.swift; sourceTree = ""; }; + 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarReusableView.swift; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -428,8 +430,9 @@ children = ( 18A3F1292BD9298900498E4A /* Calendar.swift */, 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */, - 18A3F1372BDA693000498E4A /* CalendarHeaderReusableView.swift */, 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, + 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */, + 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */, ); path = Calendar; sourceTree = ""; @@ -1163,7 +1166,6 @@ 71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */, EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */, - 18A3F1382BDA693000498E4A /* CalendarHeaderReusableView.swift in Sources */, EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */, 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, @@ -1216,7 +1218,9 @@ EA0B180A2AA78F9000F2D0CD /* UIEdgeInsets.swift in Sources */, EA985C1D296CD13600F2FF2E /* BundleManager.swift in Sources */, EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */, + 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */, EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, + 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index cb938dde..c9312c4e 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -62,8 +62,8 @@ open class CalendarBase: View { /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. open var indicators: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - /// Array of indicators for the legend. - open var indicatorData: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } +// /// Array of indicators for the legend. +// open var indicatorData: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } /// A callback when the selected date changes.. open var onChangeSelectedDate: ((Date) -> String)? @@ -76,7 +76,7 @@ open class CalendarBase: View { private let headerHeight = 104.0 private let footerHeight = 56.0 private let items = 35 - + internal var containerView = View().with { $0.clipsToBounds = true } @@ -127,6 +127,11 @@ open class CalendarBase: View { collectionView.pinToSuperView() } + open override func updateView() { + super.updateView() + collectionView.reloadData() + } + override open func layoutSubviews() { super.layoutSubviews() } @@ -163,7 +168,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { return UICollectionReusableView() } - footer.configure(with: true) + footer.configure(with: indicators) return footer } } diff --git a/VDS/Components/Calendar/CalendarIndicatorModel.swift b/VDS/Components/Calendar/CalendarIndicatorModel.swift index 04ad9714..911afe36 100644 --- a/VDS/Components/Calendar/CalendarIndicatorModel.swift +++ b/VDS/Components/Calendar/CalendarIndicatorModel.swift @@ -8,7 +8,7 @@ import Foundation /// Custom data type for indicators prop -extension CalendarBase { +//extension CalendarBase { public struct CalendarIndicatorModel { /// Text that shown to an indicator for legend @@ -22,4 +22,4 @@ extension CalendarBase { self.date = date } } -} +//} diff --git a/VDS/Components/Calendar/CalendarLegendView.swift b/VDS/Components/Calendar/CalendarLegendView.swift new file mode 100644 index 00000000..df1bbdf6 --- /dev/null +++ b/VDS/Components/Calendar/CalendarLegendView.swift @@ -0,0 +1,204 @@ +// +// CalendarLegendView.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 29/04/24. +// + +import Foundation +import UIKit +import VDSTokens +import Combine + +/// Legend view to show array of indicators as calendar footer view. +open class CalendarLegendView: View { + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var items: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 320, height: 56) } //width:320/328 + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + private let flowLayout = UICollectionViewFlowLayout().with { + $0.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + $0.minimumLineSpacing = VDSLayout.space1X + $0.minimumInteritemSpacing = VDSLayout.space4X + $0.scrollDirection = .vertical + } + open lazy var legendCollectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout).with { + $0.isScrollEnabled = false + $0.translatesAutoresizingMaskIntoConstraints = false + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.isAccessibilityElement = true + $0.backgroundColor = .clear + $0.delegate = self + $0.dataSource = self + $0.register(LegendCollectionViewCell.self, forCellWithReuseIdentifier: LegendCollectionViewCell.identifier) + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + open override func setup() { + super.setup() + isAccessibilityElement = false + + addSubview(containerView) + containerView + .pinTop(VDSLayout.space6X) + .pinBottom(VDSLayout.space4X) + .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space5X, .defaultLow) + .pinTrailingLessThanOrEqualTo(trailingAnchor, VDSLayout.space5X, .defaultHigh) + .height(containerSize.height) + .width(containerSize.width) + + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // legend Collection View + containerView.addSubview(legendCollectionView) + legendCollectionView.pinToSuperView() + + } + + open override func updateView() { + super.updateView() + legendCollectionView.reloadData() + } + + override open func layoutSubviews() { + super.layoutSubviews() + } + + open override func reset() { + super.reset() + } +} + +extension CalendarLegendView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard collectionView == legendCollectionView, + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LegendCollectionViewCell.identifier, for: indexPath) as? LegendCollectionViewCell, + indexPath.row <= items.count else { return UICollectionViewCell() } + let text = items[indexPath.row].label + cell.updateTitle(text: text, color: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullcircle: indexPath.row == 1, drawSemiCircle: indexPath.row == 2) + return cell + } +} + +private class LegendCollectionViewCell: UICollectionViewCell { + + static let identifier: String = String(describing: LegendCollectionViewCell.self) + + private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + + private var title = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .left + $0.numberOfLines = 1 + $0.textStyle = .bodySmall + $0.backgroundColor = .clear + $0.isAccessibilityElement = false + } + private var legendIndicatorWrapper: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + } + private var legendIndicator: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.layer.borderWidth = 1.0 + } + + private lazy var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.distribution = .equalSpacing + $0.spacing = VDSLayout.space2X + $0.axis = .horizontal + $0.backgroundColor = .clear + } + + private lazy var shapeLayer = CAShapeLayer() + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setupCell() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupCell() + } + + func setupCell() { + addSubview(stackView) + stackView.pinToSuperView() + + legendIndicatorWrapper.addSubview(legendIndicator) + legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + + stackView.addArrangedSubview(legendIndicatorWrapper) + stackView.addArrangedSubview(title) + } + + func updateTitle(text: String, color: UIColor, surface: Surface, clearFullcircle: Bool, drawSemiCircle: Bool) { + title.text = text + title.textColor = textColorConfiguration.getColor(surface) + + legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullcircle ? .clear : color) + legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor + + self.layoutIfNeeded() + + legendIndicator.layer.cornerRadius = legendIndicator.frame.size.height / 2.0 + + guard drawSemiCircle else { return } + + let center = CGPoint(x: legendIndicator.frame.size.width/2, y: legendIndicator.frame.size.height/2) + let path = UIBezierPath() + path.move(to: center) + path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) + path.close() + shapeLayer.path = path.cgPath + shapeLayer.fillColor = color.cgColor + + guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return } + legendIndicator.layer.addSublayer(shapeLayer) + } +} diff --git a/VDS/Components/Calendar/CalendarHeaderReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift similarity index 83% rename from VDS/Components/Calendar/CalendarHeaderReusableView.swift rename to VDS/Components/Calendar/CalendarReusableView.swift index 3f139a10..8131ce33 100644 --- a/VDS/Components/Calendar/CalendarHeaderReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -1,11 +1,12 @@ // -// CalendarHeaderReusableView.swift +// CalendarReusableView.swift // VDS // // Created by Kanamarlapudi, Vasavi on 24/04/24. // import UIKit +import VDSTokens /// Custom header view class CalendarHeaderReusableView: UICollectionReusableView { @@ -23,7 +24,6 @@ class CalendarHeaderReusableView: UICollectionReusableView { func configure(with color: Bool) { // Make a view and make in generic and dynamic - self.backgroundColor = .orange } override func layoutSubviews() { @@ -37,6 +37,8 @@ class CalendarFooterReusableView: UICollectionReusableView { ///Identifier for the Calendar Footer Reusable View static let identifier: String = String(describing: CalendarFooterReusableView.self) + private lazy var footerView = CalendarLegendView() + override init(frame: CGRect) { super.init(frame: frame) } @@ -45,13 +47,12 @@ class CalendarFooterReusableView: UICollectionReusableView { fatalError("init(coder:) has not been implemented") } - func configure(with color: Bool) { - // Make a view and make in generic and dynamic - self.backgroundColor = .green + func configure(with indicators: [CalendarIndicatorModel]) { + footerView.items = indicators + addSubview(footerView) } override func layoutSubviews() { super.layoutSubviews() } } - From ef80db9e73c22888208b3c607784c364abc86175 Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 30 Apr 2024 14:21:43 +0530 Subject: [PATCH 06/87] Digital ACT-191 ONEAPP-7016 story: minor changes --- VDS/Components/Calendar/Calendar.swift | 1 + VDS/Components/Calendar/CalendarIndicatorModel.swift | 4 ++-- VDS/Components/Calendar/CalendarLegendView.swift | 4 +++- VDS/Components/Calendar/CalendarReusableView.swift | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index c9312c4e..a7ed560a 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -90,6 +90,7 @@ open class CalendarBase: View { collectionView.dataSource = self collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false + collectionView.backgroundColor = .clear collectionView.register(CalendarDateCollectionViewCell.self, forCellWithReuseIdentifier: CalendarDateCollectionViewCell.identifier) collectionView.register(CalendarHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, diff --git a/VDS/Components/Calendar/CalendarIndicatorModel.swift b/VDS/Components/Calendar/CalendarIndicatorModel.swift index 911afe36..04ad9714 100644 --- a/VDS/Components/Calendar/CalendarIndicatorModel.swift +++ b/VDS/Components/Calendar/CalendarIndicatorModel.swift @@ -8,7 +8,7 @@ import Foundation /// Custom data type for indicators prop -//extension CalendarBase { +extension CalendarBase { public struct CalendarIndicatorModel { /// Text that shown to an indicator for legend @@ -22,4 +22,4 @@ import Foundation self.date = date } } -//} +} diff --git a/VDS/Components/Calendar/CalendarLegendView.swift b/VDS/Components/Calendar/CalendarLegendView.swift index df1bbdf6..78826ffd 100644 --- a/VDS/Components/Calendar/CalendarLegendView.swift +++ b/VDS/Components/Calendar/CalendarLegendView.swift @@ -30,7 +30,7 @@ open class CalendarLegendView: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - open var items: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + open var items: [CalendarBase.CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties @@ -90,6 +90,8 @@ open class CalendarLegendView: View { open override func updateView() { super.updateView() legendCollectionView.reloadData() + setNeedsLayout() + layoutIfNeeded() } override open func layoutSubviews() { diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index 8131ce33..96995006 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -47,7 +47,7 @@ class CalendarFooterReusableView: UICollectionReusableView { fatalError("init(coder:) has not been implemented") } - func configure(with indicators: [CalendarIndicatorModel]) { + func configure(with indicators: [CalendarBase.CalendarIndicatorModel]) { footerView.items = indicators addSubview(footerView) } From 23831589f0dba211193205d47b51a43424f0e6be Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 30 Apr 2024 19:19:28 +0530 Subject: [PATCH 07/87] Digital ACT-191 ONEAPP-7016 story: added header view and updated with week days --- VDS.xcodeproj/project.pbxproj | 8 + VDS/Components/Calendar/Calendar.swift | 13 +- .../Calendar/CalendarHeaderView.swift | 195 ++++++++++++++++++ .../Calendar/CalendarReusableView.swift | 4 +- VDS/Components/Calendar/Date+Extension.swift | 20 ++ 5 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 VDS/Components/Calendar/CalendarHeaderView.swift create mode 100644 VDS/Components/Calendar/Date+Extension.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 514e3fa0..33835770 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */; }; 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */; }; + 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; + 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; @@ -219,6 +221,8 @@ 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarLegendView.swift; sourceTree = ""; }; 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarReusableView.swift; sourceTree = ""; }; + 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; + 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -433,6 +437,8 @@ 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */, 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */, + 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */, + 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, ); path = Calendar; sourceTree = ""; @@ -1170,6 +1176,7 @@ 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, + 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */, EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */, @@ -1239,6 +1246,7 @@ 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */, + 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */, EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */, EAB5FED429267EB300998C17 /* UIView+NSLayoutConstraint.swift in Sources */, EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index a7ed560a..0700bb55 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -101,6 +101,15 @@ open class CalendarBase: View { return collectionView }() + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + internal var containerBorderColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight , VDSColor.elementsPrimaryOndark) + internal var backgroundColorConfiguration = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) + $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) + } + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -122,7 +131,8 @@ open class CalendarBase: View { .width(containerSize.width) containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - + containerView.layer.borderWidth = VDSFormControls.borderWidth + // Calendar View containerView.addSubview(collectionView) collectionView.pinToSuperView() @@ -131,6 +141,7 @@ open class CalendarBase: View { open override func updateView() { super.updateView() collectionView.reloadData() + containerView.layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor } override open func layoutSubviews() { diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift new file mode 100644 index 00000000..2e591b94 --- /dev/null +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -0,0 +1,195 @@ +// +// CalendarHeaderView.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 30/04/24. +// + +import Foundation +import UIKit +import VDSTokens + +/// Header view to display month and year along with days of week +open class CalendarHeaderView: View { + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 320, height: 104) } //width:320/328 + + private lazy var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.distribution = .fill + $0.spacing = VDSLayout.space1X + $0.axis = .vertical + $0.backgroundColor = .clear + } + + private lazy var monthHeaderStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.distribution = .fill + $0.spacing = VDSLayout.space1X + $0.axis = .horizontal + $0.backgroundColor = .clear + } + + internal var containerView = View().with { + $0.clipsToBounds = true + $0.backgroundColor = .clear + } + internal var monthView = View() + internal var daysView = View() + internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays + + private lazy var daysCollectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collectionView.isScrollEnabled = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.backgroundColor = .clear + collectionView.register(collectionViewCell.self, forCellWithReuseIdentifier: collectionViewCell.identifier) + return collectionView + }() + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + open override func setup() { + super.setup() + isAccessibilityElement = false + + // stackView + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) + .width(containerSize.width) + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // stackview + containerView.addSubview(stackView) + stackView + .pinTop(VDSLayout.space4X) + .pinLeading() + .pinTrailing() + .pinBottom(VDSLayout.space1X) + + // month label view, previous and next buttons + stackView.addArrangedSubview(monthView) + monthView.backgroundColor = .orange + monthView.heightAnchor.constraint(equalToConstant: 40).isActive = true + + // days Collection View + stackView.addArrangedSubview(daysCollectionView) + daysCollectionView.widthAnchor.constraint(equalTo: widthAnchor).activate() + daysCollectionView.heightAnchor.constraint(equalTo: monthView.heightAnchor).activate() + print("daysOfWeek: \(daysOfWeek)") + } + + open override func updateView() { + super.updateView() + daysCollectionView.reloadData() + } + + override open func layoutSubviews() { + super.layoutSubviews() + } + + open override func reset() { + super.reset() + } +} + +extension CalendarHeaderView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return daysOfWeek.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionViewCell.identifier, for: indexPath) as? collectionViewCell else { return UICollectionViewCell() } + cell.updateTitle(text: daysOfWeek[indexPath.row], surface: surface) + return cell + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 40, height: 40) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } +} + +private class collectionViewCell: UICollectionViewCell { + + static let identifier: String = String(describing: collectionViewCell.self) + + private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + + private var title = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.numberOfLines = 1 + $0.textStyle = .bodySmall + $0.backgroundColor = .clear + $0.isAccessibilityElement = false + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setupCell() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupCell() + } + + func setupCell() { + addSubview(title) + title.pinToSuperView() + } + + func updateTitle(text: String, surface: Surface) { + title.surface = surface + title.text = text + title.textColor = textColorConfiguration.getColor(surface) + title.backgroundColor = .clear + } +} diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index 96995006..b348cc30 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -14,6 +14,8 @@ class CalendarHeaderReusableView: UICollectionReusableView { ///Identifier for the Calendar Header Reusable View static let identifier: String = String(describing: CalendarHeaderReusableView.self) + private lazy var headerView = CalendarHeaderView() + override init(frame: CGRect) { super.init(frame: frame) } @@ -23,7 +25,7 @@ class CalendarHeaderReusableView: UICollectionReusableView { } func configure(with color: Bool) { - // Make a view and make in generic and dynamic + addSubview(headerView) } override func layoutSubviews() { diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift new file mode 100644 index 00000000..60036ee2 --- /dev/null +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -0,0 +1,20 @@ +// +// Date+Extension.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 26/04/24. +// + +import Foundation + +extension Date { + static var capitalizedFirstLettersOfWeekdays: [String] { + let calendar = Calendar.current + let weekdays = calendar.shortWeekdaySymbols + + return weekdays.map { weekday in + guard let firstLetter = weekday.first else { return "" } + return String(firstLetter).capitalized + } + } +} From 36238890c79cf227ebfebb3d473a2f40614616bf Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 30 Apr 2024 19:39:50 +0530 Subject: [PATCH 08/87] Digital ACT-191 ONEAPP-7016 story: added change log --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Components/Calendar/CalendarChangeLog.txt | 84 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 VDS/Components/Calendar/CalendarChangeLog.txt diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 33835770..e75dc4c1 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */; }; + 18FEA1B92BE1301700A56439 /* CalendarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; @@ -223,6 +224,7 @@ 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarReusableView.swift; sourceTree = ""; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = ""; }; + 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CalendarChangeLog.txt; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -439,6 +441,7 @@ 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */, 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */, 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, + 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */, ); path = Calendar; sourceTree = ""; @@ -1094,6 +1097,7 @@ EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */, EAEEECA92B1F969700531FC2 /* TooltipChangeLog.txt in Resources */, 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */, + 18FEA1B92BE1301700A56439 /* CalendarChangeLog.txt in Resources */, EAEEEC9C2B1F8F0700531FC2 /* TextLinkCaretChangeLog.txt in Resources */, EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */, 71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */, diff --git a/VDS/Components/Calendar/CalendarChangeLog.txt b/VDS/Components/Calendar/CalendarChangeLog.txt new file mode 100644 index 00000000..ee6928a3 --- /dev/null +++ b/VDS/Components/Calendar/CalendarChangeLog.txt @@ -0,0 +1,84 @@ +MM/DD/YYYY +---------------- +- Initial Brand 3.0 handoff + +12/24/2021 +---------------- +- Replaced focusring colors (previously interactive/onlight/ondark) with accessibility/onlight/ondark colors +- Updated focus border name (previously interactive.focusring.onlight) with focusring.onlight/ondark +- Updated the SPECS with FormControl tokens and corner radius tokens + +02/24/2022 +---------------- +- Replaced Caret Left and Right icons with bold assets. + +02/28/2022 +---------------- +- Removed dev note from Hover state and added a dev note to Active state. + +03/11/2022 +---------------- +- Update Hover and Active states triggers for carets. Icon swap and color change for mouse-only, and color change for touch. + +05/25/2022 +---------------- +- Added date indicator feature (including legend), today’s date indicator, and size/spacing adjustments to support this. + +07/20/2022 +---------------- +- Added configuration page. +- To configuration added transparent background and border suppression. + +08/10/2022 +---------------- +- Updated inverted and default to light and dark surface. Also, updated dark to selected. + +08/16/2022 +---------------- +- Updated default date background to be transparent. Updated border configuration prop name to hideContainerBorder. +- Moved Width from Configurations to Layout and Spacing, and Calendar Indicator specs under Elements. + +11/30/2022 +---------------- +- Added "(web only)" to any instance of "keyboard focus" + +12/13/2022 +---------------- +- Replaced form border and focus border pixel values, styles & spacing with tokens. + +01/09/2023 +---------------- +- Updated Specs to use new SPEC Templates and SPEC DOC Components. + +01/10/2023 +---------------- +- Updated Anatomy item #8 to “Current Date” to match design doc (originally “Today’s Date) + +04/12/2023 +---------------- +- Updated palette colors for Current date. + +05/01/2023 +---------------- +- Updated Day Header Text Style from Bold to Regular +- Updated Date Disabled Text Style from Bold to Regular +- Updated Date Text Style from Bold to Regular +- Updated Current Date font color from blue to black on light and white on dark. +- Updated all frames to reflect new designs. + +05/09/2023 +---------------- +- Replaced Previous and Next carets with Button Icon in: +Anatomy +Configurations +States + +11/09/2023 +---------------- +- Added component tokens +- Applied component tokens to selected states on light and dark surfaces +- Removed redundant color specifications from other sections + +11/30/2023 +---------------- +- Revised selected container background inverse tokens from onlight/ondark to light/dark From 0d9c409736c357c69fb0981b029e3587ca4cba6b Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 2 May 2024 07:53:42 +0530 Subject: [PATCH 09/87] Digital ACT-191 ONEAPP-7016 story: updated constraints and header month view --- VDS/Components/Calendar/Calendar.swift | 18 +++++-- .../Calendar/CalendarHeaderView.swift | 54 +++++++++++++++++-- .../Calendar/CalendarLegendView.swift | 8 +-- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 0700bb55..3fa9de4a 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -71,10 +71,10 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 320, height: 376) } //width:320/328 + internal var containerSize: CGSize { CGSize(width: 328, height: 376) } //width:320/328 private let cellItemSize = CGSize(width: 40, height: 40) - private let headerHeight = 104.0 - private let footerHeight = 56.0 + private let headerHeight = 88.0 + private let footerHeight = 40.0 private let items = 35 internal var containerView = View().with { @@ -135,7 +135,17 @@ open class CalendarBase: View { // Calendar View containerView.addSubview(collectionView) - collectionView.pinToSuperView() + collectionView + .pinTop(VDSLayout.space4X) + .pinBottom(VDSLayout.space4X) + .pinLeading(VDSFormControls.spaceInset) + .pinTrailing(VDSFormControls.spaceInset) + let width = containerSize.width - (2 * VDSFormControls.spaceInset) + collectionView.widthAnchor.constraint(equalToConstant: width).activate() + let height = containerSize.height - (2 * VDSLayout.space4X) + collectionView.heightAnchor.constraint(equalToConstant: height).activate() + collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() + collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() } open override func updateView() { diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index 2e591b94..bba9805c 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -33,7 +33,7 @@ open class CalendarHeaderView: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 320, height: 104) } //width:320/328 + internal var containerSize: CGSize { CGSize(width: 304, height: 88) } //width:320/328 private lazy var stackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -56,9 +56,36 @@ open class CalendarHeaderView: View { $0.backgroundColor = .clear } internal var monthView = View() + internal var previousMonthView = View() + internal var nextMonthView = View() internal var daysView = View() internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays + internal var previousButton = ButtonIcon().with { + $0.kind = .ghost + $0.iconName = .leftCaret + $0.iconOffset = .init(x: -2, y: 0) + $0.icon.size = .small + $0.size = .small + } + + internal var nextButton = ButtonIcon().with { + $0.kind = .ghost + $0.iconName = .rightCaret + $0.iconOffset = .init(x: 2, y: 0) + $0.icon.size = .small + $0.size = .small + } + + internal var monthYearLabel = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.numberOfLines = 1 + $0.textStyle = .boldBodySmall + $0.backgroundColor = .clear + $0.isAccessibilityElement = false + } + private lazy var daysCollectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) collectionView.isScrollEnabled = false @@ -72,6 +99,8 @@ open class CalendarHeaderView: View { return collectionView }() + internal var monthYearLabelTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -97,16 +126,33 @@ open class CalendarHeaderView: View { // stackview containerView.addSubview(stackView) stackView - .pinTop(VDSLayout.space4X) + .pinTop() .pinLeading() .pinTrailing() .pinBottom(VDSLayout.space1X) // month label view, previous and next buttons stackView.addArrangedSubview(monthView) - monthView.backgroundColor = .orange + monthView.backgroundColor = .clear monthView.heightAnchor.constraint(equalToConstant: 40).isActive = true + // month Header stack view + monthView.addSubview(monthHeaderStackView) + monthHeaderStackView.pinToSuperView() + + // previous button + monthHeaderStackView.addArrangedSubview(previousMonthView) + previousMonthView.widthAnchor.constraint(equalToConstant: 40).activate() + previousMonthView.addSubview(previousButton) + + // month year label + monthHeaderStackView.addArrangedSubview(monthYearLabel) + + // next button + monthHeaderStackView.addArrangedSubview(nextMonthView) + nextMonthView.widthAnchor.constraint(equalToConstant: 40).activate() + nextMonthView.addSubview(nextButton) + // days Collection View stackView.addArrangedSubview(daysCollectionView) daysCollectionView.widthAnchor.constraint(equalTo: widthAnchor).activate() @@ -117,6 +163,8 @@ open class CalendarHeaderView: View { open override func updateView() { super.updateView() daysCollectionView.reloadData() + monthYearLabel.text = "May 2024" + monthYearLabel.textColor = monthYearLabelTextColorConfiguration.getColor(surface) } override open func layoutSubviews() { diff --git a/VDS/Components/Calendar/CalendarLegendView.swift b/VDS/Components/Calendar/CalendarLegendView.swift index 78826ffd..9345483d 100644 --- a/VDS/Components/Calendar/CalendarLegendView.swift +++ b/VDS/Components/Calendar/CalendarLegendView.swift @@ -35,7 +35,7 @@ open class CalendarLegendView: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 320, height: 56) } //width:320/328 + internal var containerSize: CGSize { CGSize(width: 304, height: 40) } //width:320/328 internal var containerView = View().with { $0.clipsToBounds = true @@ -73,9 +73,9 @@ open class CalendarLegendView: View { addSubview(containerView) containerView .pinTop(VDSLayout.space6X) - .pinBottom(VDSLayout.space4X) - .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space5X, .defaultLow) - .pinTrailingLessThanOrEqualTo(trailingAnchor, VDSLayout.space5X, .defaultHigh) + .pinBottom() + .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space3X, .defaultLow) + .pinTrailingLessThanOrEqualTo(trailingAnchor, VDSLayout.space3X, .defaultHigh) .height(containerSize.height) .width(containerSize.width) From 408effa9f0c9ea7fd8e9c27e035f3be32e50246d Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 2 May 2024 17:51:56 +0530 Subject: [PATCH 10/87] Digital ACT-191 ONEAPP-7016 story: displaying days for calendar month view --- VDS.xcodeproj/project.pbxproj | 10 +- VDS/Components/Calendar/Calendar.swift | 49 ++++- .../CalendarDateCollectionViewCell.swift | 193 +++++++++++++++--- ...endView.swift => CalendarFooterView.swift} | 8 +- .../Calendar/CalendarHeaderView.swift | 12 +- .../Calendar/CalendarReusableView.swift | 2 +- VDS/Components/Calendar/Date+Extension.swift | 45 ++++ 7 files changed, 271 insertions(+), 48 deletions(-) rename VDS/Components/Calendar/{CalendarLegendView.swift => CalendarFooterView.swift} (97%) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index e75dc4c1..e34e7772 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; + 18408EDE2BE32C9900E8646B /* CalendarFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */; }; 18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */; }; 1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; }; 186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; }; @@ -23,7 +24,6 @@ 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; - 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */; }; 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */; }; @@ -207,6 +207,7 @@ 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = ""; }; 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; + 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterView.swift; sourceTree = ""; }; 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = ""; }; 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = ""; }; 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = ""; }; @@ -220,7 +221,6 @@ 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; - 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarLegendView.swift; sourceTree = ""; }; 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarReusableView.swift; sourceTree = ""; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = ""; }; @@ -436,11 +436,11 @@ children = ( 18A3F1292BD9298900498E4A /* Calendar.swift */, 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */, + 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */, + 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, - 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */, 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */, 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */, - 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */, ); path = Calendar; @@ -1216,6 +1216,7 @@ EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */, EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */, EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */, + 18408EDE2BE32C9900E8646B /* CalendarFooterView.swift in Sources */, EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */, EAA7456C2AB23E2000C1841F /* TooltipModel.swift in Sources */, EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, @@ -1229,7 +1230,6 @@ EA0B180A2AA78F9000F2D0CD /* UIEdgeInsets.swift in Sources */, EA985C1D296CD13600F2FF2E /* BundleManager.swift in Sources */, EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */, - 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */, EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 3fa9de4a..faa1bed4 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -54,7 +54,18 @@ open class CalendarBase: View { /// If provided, this is the date that will show as selected by the Calendar. /// If no value is provided, the current date will be used. If null is provided, no date will be selected. - open var selectedDate: Date? + open var selectedDate: Date? { + get { return _selectedDate } + set { + if let newValue { + _selectedDate = newValue + } else { + _selectedDate = Date() + } + //update views what needed + setNeedsUpdate() + } + } /// If provided, the calendar will be rendered with transparent background. open var transparentBackground: Bool = false @@ -71,6 +82,9 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal var _selectedDate: Date = Date() + private var dates: [Date] = [] + private var days: [String] = [] internal var containerSize: CGSize { CGSize(width: 328, height: 376) } //width:320/328 private let cellItemSize = CGSize(width: 40, height: 40) private let headerHeight = 88.0 @@ -150,6 +164,7 @@ open class CalendarBase: View { open override func updateView() { super.updateView() + self.fetchDates(with: selectedDate ?? Date()) collectionView.reloadData() containerView.layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor } @@ -161,6 +176,30 @@ open class CalendarBase: View { open override func reset() { super.reset() } + + func fetchDates(with date:Date) { + days.removeAll() + if let dates = selectedDate?.calendarDisplayDays { + self.dates = dates + for day in dates { + // code to be executed + if day.monthInt != selectedDate?.monthInt { + days.append("") + } else { + if #available(iOS 15.0, *) { + days.append(day.formatted(.dateTime.day())) + } else { + // Fallback on earlier versions + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "d" + let dayStr: String = dateFormatter.string(from: day) + days.append(dayStr) + } + } + } +// print("days: \(days)") + } + } } extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { @@ -168,11 +207,12 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI // MARK: - UICollectionView Delegate & Datasource //-------------------------------------------------- public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - items + days.count } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } + cell.configure(with: indicators, text: days[indexPath.row]) return cell } @@ -197,6 +237,11 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI return UICollectionReusableView() } + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let date = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! + print("selected day: \(days[indexPath.row]), date: \(date)") + } + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: headerHeight) } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index 156cce04..f1c50f15 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -9,46 +9,179 @@ import Foundation import UIKit import VDSTokens -///This is customised view for Calendar cell item +/// Calendar collection view cell final class CalendarDateCollectionViewCell: UICollectionViewCell { ///Identifier for the Calendar Date Cell static let identifier: String = String(describing: CalendarDateCollectionViewCell.self) - //-------------------------------------------------- - // MARK: - Private Properties - //-------------------------------------------------- -// internal var stackView: UIStackView = { -// return UIStackView().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.axis = .horizontal -// $0.distribution = .fill -// $0.alignment = .center -// $0.spacing = VDSLayout.space2X -// $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) -// $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) -// } -// }() -// -// private lazy var selectionBackgroundView = View().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.clipsToBounds = true -// $0.backgroundColor = .systemRed -// } -// -// private lazy var numberLabel = Label().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.textAlignment = .center -// // $0.font -// // $0.textColor -// } - override init(frame:CGRect) { + private lazy var dateView = DateView() + + override init(frame: CGRect) { super.init(frame: frame) - contentView.backgroundColor = .link } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func configure(with indicators: [CalendarBase.CalendarIndicatorModel], text: String) { + addSubview(dateView) + dateView.dateIndicators = indicators + dateView.numberLabel.text = text + } + + override func layoutSubviews() { + super.layoutSubviews() + } +} + +/// Date view to show Date number and indicator if applies +private class DateView : View { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var dateIndicators: [CalendarBase.CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + + open var numberLabel = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.textStyle = .bodySmall //isCurrentDate: .boldBodySmall + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 40, height: 40) } + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + private lazy var stackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .center + $0.spacing = VDSLayout.space2X + $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) + } + }() + +// private lazy var indicatorsView = View().with { +// $0.translatesAutoresizingMaskIntoConstraints = false +// $0.clipsToBounds = true +// $0.backgroundColor = .systemRed +// } + + private var legendIndicator: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.layer.borderWidth = 1.0 + } + + private lazy var shapeLayer = CAShapeLayer() + + private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + open override func setup() { + super.setup() + isAccessibilityElement = false + + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) + .width(containerSize.width) + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // Number label + containerView.addSubview(numberLabel) + numberLabel.pinToSuperView() + + // Indicators + if dateIndicators.count > 0 { + containerView.addSubview(stackView) + let topPos = containerSize.height * 0.6 + stackView.pinTop(topPos).pinBottom().pinLeading().pinTrailing().pinCenterY() + stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + let width = (dateIndicators.count * 8) + (8 * (dateIndicators.count - 1)) + stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() + } + } + + open override func updateView() { + super.updateView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + } + + open override func reset() { + super.reset() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + func configure(with indicators: [CalendarBase.CalendarIndicatorModel]) { + //TO DO: handling indicators - in progress + //show indicator + legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + stackView.addArrangedSubview(legendIndicator) + updateIndicator(with: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullCircle: false, drawSemiCircle: false) + } + + func updateIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool){ + legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) + legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor + + self.layoutIfNeeded() + + legendIndicator.layer.cornerRadius = legendIndicator.frame.size.height / 2.0 + + guard drawSemiCircle else { return } + + let center = CGPoint(x: legendIndicator.frame.size.width/2, y: legendIndicator.frame.size.height/2) + let path = UIBezierPath() + path.move(to: center) + path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) + path.close() + shapeLayer.path = path.cgPath + shapeLayer.fillColor = color.cgColor + + guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return } + legendIndicator.layer.addSublayer(shapeLayer) + } } diff --git a/VDS/Components/Calendar/CalendarLegendView.swift b/VDS/Components/Calendar/CalendarFooterView.swift similarity index 97% rename from VDS/Components/Calendar/CalendarLegendView.swift rename to VDS/Components/Calendar/CalendarFooterView.swift index 9345483d..84d76cab 100644 --- a/VDS/Components/Calendar/CalendarLegendView.swift +++ b/VDS/Components/Calendar/CalendarFooterView.swift @@ -1,5 +1,5 @@ // -// CalendarLegendView.swift +// CalendarFooterView.swift // VDS // // Created by Kanamarlapudi, Vasavi on 29/04/24. @@ -10,8 +10,8 @@ import UIKit import VDSTokens import Combine -/// Legend view to show array of indicators as calendar footer view. -open class CalendarLegendView: View { +/// Footer view to show indicators data. +open class CalendarFooterView: View { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -103,7 +103,7 @@ open class CalendarLegendView: View { } } -extension CalendarLegendView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +extension CalendarFooterView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return items.count diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index bba9805c..44f5462f 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -43,7 +43,7 @@ open class CalendarHeaderView: View { $0.backgroundColor = .clear } - private lazy var monthHeaderStackView = UIStackView().with { + private lazy var monthYearHeaderStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.distribution = .fill $0.spacing = VDSLayout.space1X @@ -137,19 +137,19 @@ open class CalendarHeaderView: View { monthView.heightAnchor.constraint(equalToConstant: 40).isActive = true // month Header stack view - monthView.addSubview(monthHeaderStackView) - monthHeaderStackView.pinToSuperView() + monthView.addSubview(monthYearHeaderStackView) + monthYearHeaderStackView.pinToSuperView() // previous button - monthHeaderStackView.addArrangedSubview(previousMonthView) + monthYearHeaderStackView.addArrangedSubview(previousMonthView) previousMonthView.widthAnchor.constraint(equalToConstant: 40).activate() previousMonthView.addSubview(previousButton) // month year label - monthHeaderStackView.addArrangedSubview(monthYearLabel) + monthYearHeaderStackView.addArrangedSubview(monthYearLabel) // next button - monthHeaderStackView.addArrangedSubview(nextMonthView) + monthYearHeaderStackView.addArrangedSubview(nextMonthView) nextMonthView.widthAnchor.constraint(equalToConstant: 40).activate() nextMonthView.addSubview(nextButton) diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index b348cc30..33bedfe2 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -39,7 +39,7 @@ class CalendarFooterReusableView: UICollectionReusableView { ///Identifier for the Calendar Footer Reusable View static let identifier: String = String(describing: CalendarFooterReusableView.self) - private lazy var footerView = CalendarLegendView() + private lazy var footerView = CalendarFooterView() override init(frame: CGRect) { super.init(frame: frame) diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 60036ee2..07284507 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -8,6 +8,7 @@ import Foundation extension Date { + static var firstDayOfWeek = Calendar.current.firstWeekday static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current let weekdays = calendar.shortWeekdaySymbols @@ -17,4 +18,48 @@ extension Date { return String(firstLetter).capitalized } } + + var startOfMonth: Date { + Calendar.current.dateInterval(of: .month, for: self)!.start + } + + var endOfMonth: Date { + let lastDay = Calendar.current.dateInterval(of: .month, for: self)!.end + return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! + } + + var numberOfDaysInMonth: Int { + Calendar.current.component(.day, from: endOfMonth) + } + + var firstWeekDayBeforeStart: Date { + let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth) + var numberFromPreviousMonth = startOfMonthWeekday - Self.firstDayOfWeek + if numberFromPreviousMonth < 0 { + numberFromPreviousMonth += 7 // Adjust to a 0-6 range if negative + } + return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! + } + + var calendarDisplayDays: [Date] { + var days: [Date] = [] + // Start with days from the previous month to fill the grid + let firstDisplayDay = firstWeekDayBeforeStart + var day = firstDisplayDay + while day < startOfMonth { + days.append(day) + day = Calendar.current.date(byAdding: .day, value: 1, to: day)! + } + // Add days of the current month + for dayOffset in 0.. Date: Fri, 3 May 2024 18:29:32 +0530 Subject: [PATCH 11/87] Digital ACT-191 ONEAPP-7016 story: Displays indicator for day based on the indicators data --- VDS/Components/Calendar/Calendar.swift | 51 +++++--- .../CalendarDateCollectionViewCell.swift | 112 ++++++++++-------- 2 files changed, 95 insertions(+), 68 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index faa1bed4..f9cb16cf 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -134,7 +134,6 @@ open class CalendarBase: View { open override func setup() { super.setup() isAccessibilityElement = false - addSubview(containerView) containerView .pinTop() @@ -147,19 +146,20 @@ open class CalendarBase: View { containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() containerView.layer.borderWidth = VDSFormControls.borderWidth + let spacing = CGFloat(VDSLayout.space2X) //CGFloat(VDSFormControls.spaceInset) // + // Calendar View containerView.addSubview(collectionView) collectionView .pinTop(VDSLayout.space4X) .pinBottom(VDSLayout.space4X) - .pinLeading(VDSFormControls.spaceInset) - .pinTrailing(VDSFormControls.spaceInset) - let width = containerSize.width - (2 * VDSFormControls.spaceInset) + .pinLeading(spacing) + .pinTrailing(spacing) + let width = containerSize.width - (2 * spacing) collectionView.widthAnchor.constraint(equalToConstant: width).activate() let height = containerSize.height - (2 * VDSLayout.space4X) collectionView.heightAnchor.constraint(equalToConstant: height).activate() collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() - collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() } open override func updateView() { @@ -177,29 +177,36 @@ open class CalendarBase: View { super.reset() } + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- func fetchDates(with date:Date) { days.removeAll() if let dates = selectedDate?.calendarDisplayDays { self.dates = dates - for day in dates { + for date in dates { // code to be executed - if day.monthInt != selectedDate?.monthInt { + if date.monthInt != selectedDate?.monthInt { days.append("") } else { - if #available(iOS 15.0, *) { - days.append(day.formatted(.dateTime.day())) - } else { - // Fallback on earlier versions - let dateFormatter: DateFormatter = DateFormatter() - dateFormatter.dateFormat = "d" - let dayStr: String = dateFormatter.string(from: day) - days.append(dayStr) - } + days.append(getDay(with: date)) } } -// print("days: \(days)") } } + + func getDay(with date:Date) -> String { + if #available(iOS 15.0, *) { + return date.formatted(.dateTime.day()) + } else { + // Fallback on earlier versions + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "d" + let day: String = dateFormatter.string(from: date) + return day + } + } + } extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { @@ -212,7 +219,13 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } - cell.configure(with: indicators, text: days[indexPath.row]) + var indicatorCount = 0 + for x in (0...(self.indicators.count-1)) { + if (self.days[indexPath.row] == self.getDay(with: self.indicators[x].date)) { + indicatorCount += 1 + } + } + cell.configure(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount) return cell } @@ -239,7 +252,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let date = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! - print("selected day: \(days[indexPath.row]), date: \(date)") +// print("selected day: \(days[indexPath.row]), date: \(date)") } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index f1c50f15..fd33611b 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -26,10 +26,11 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func configure(with indicators: [CalendarBase.CalendarIndicatorModel], text: String) { + func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int) { addSubview(dateView) - dateView.dateIndicators = indicators dateView.numberLabel.text = text + dateView.indicatorCount = indicatorCount + dateView.dateIndicators = indicators } override func layoutSubviews() { @@ -58,8 +59,10 @@ private class DateView : View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- + open var indicatorCount: Int = 0 + open var dateIndicators: [CalendarBase.CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - + open var numberLabel = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center @@ -70,39 +73,26 @@ private class DateView : View { // MARK: - Private Properties //-------------------------------------------------- internal var containerSize: CGSize { CGSize(width: 40, height: 40) } - + internal var containerView = View().with { $0.clipsToBounds = true } - + private lazy var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill $0.alignment = .center - $0.spacing = VDSLayout.space2X + $0.spacing = VDSLayout.space1X $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) } }() -// private lazy var indicatorsView = View().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.clipsToBounds = true -// $0.backgroundColor = .systemRed -// } - - private var legendIndicator: View = View().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.backgroundColor = .clear - $0.layer.borderWidth = 1.0 - } - private lazy var shapeLayer = CAShapeLayer() - - private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) //-------------------------------------------------- // MARK: - Lifecycle @@ -114,7 +104,6 @@ private class DateView : View { open override func setup() { super.setup() isAccessibilityElement = false - addSubview(containerView) containerView .pinTop() @@ -124,24 +113,33 @@ private class DateView : View { .height(containerSize.height) .width(containerSize.width) containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - + // Number label containerView.addSubview(numberLabel) numberLabel.pinToSuperView() - + // Indicators - if dateIndicators.count > 0 { - containerView.addSubview(stackView) - let topPos = containerSize.height * 0.6 - stackView.pinTop(topPos).pinBottom().pinLeading().pinTrailing().pinCenterY() - stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - let width = (dateIndicators.count * 8) + (8 * (dateIndicators.count - 1)) - stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() - } + containerView.addSubview(stackView) + let topPos = containerSize.height * 0.6 + stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo().pinCenterY() + stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } open override func updateView() { super.updateView() + numberLabel.surface = surface + numberLabel.isEnabled = isEnabled + + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + if indicatorCount > 0 { + let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) + stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() + for x in (0...(dateIndicators.count-1)) { + if (self.numberLabel.text == self.getDay(with: dateIndicators[x].date)) { + addIndicator(with: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + } + } + } } override open func layoutSubviews() { @@ -150,30 +148,33 @@ private class DateView : View { open override func reset() { super.reset() + numberLabel.textStyle = .bodySmall } - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- - func configure(with indicators: [CalendarBase.CalendarIndicatorModel]) { - //TO DO: handling indicators - in progress - //show indicator - legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() - stackView.addArrangedSubview(legendIndicator) - updateIndicator(with: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullCircle: false, drawSemiCircle: false) - } - - func updateIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool){ - legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) - legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + func addIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool) { + // add indicator + let indicatorView: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.layer.borderWidth = 1.0 + } + indicatorView.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + stackView.addArrangedSubview(indicatorView) + + // update indicator + indicatorView.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) + indicatorView.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor self.layoutIfNeeded() - legendIndicator.layer.cornerRadius = legendIndicator.frame.size.height / 2.0 + indicatorView.layer.cornerRadius = indicatorView.frame.size.height / 2.0 guard drawSemiCircle else { return } - let center = CGPoint(x: legendIndicator.frame.size.width/2, y: legendIndicator.frame.size.height/2) + let center = CGPoint(x: indicatorView.frame.size.width/2, y: indicatorView.frame.size.height/2) let path = UIBezierPath() path.move(to: center) path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) @@ -181,7 +182,20 @@ private class DateView : View { shapeLayer.path = path.cgPath shapeLayer.fillColor = color.cgColor - guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return } - legendIndicator.layer.addSublayer(shapeLayer) + guard indicatorView.layer.sublayers?.contains(shapeLayer) ?? true else { return } + indicatorView.layer.addSublayer(shapeLayer) } + + func getDay(with date:Date) -> String { + if #available(iOS 15.0, *) { + return date.formatted(.dateTime.day()) + } else { + // Fallback on earlier versions + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "d" + let day: String = dateFormatter.string(from: date) + return day + } + } + } From f043ac74d38787e390148ab6a0ac1bc068f4117b Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 3 May 2024 19:59:49 +0530 Subject: [PATCH 12/87] Digital ACT-191 ONEAPP-7016 story: updating surface for all childviews --- VDS/Components/Calendar/Calendar.swift | 8 ++++---- .../Calendar/CalendarDateCollectionViewCell.swift | 3 ++- VDS/Components/Calendar/CalendarFooterView.swift | 5 +++-- VDS/Components/Calendar/CalendarHeaderView.swift | 6 +++++- VDS/Components/Calendar/CalendarReusableView.swift | 6 ++++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index f9cb16cf..3e1442e1 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -146,7 +146,7 @@ open class CalendarBase: View { containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() containerView.layer.borderWidth = VDSFormControls.borderWidth - let spacing = CGFloat(VDSLayout.space2X) //CGFloat(VDSFormControls.spaceInset) // + let spacing = CGFloat(VDSLayout.space2X) //CGFloat(VDSFormControls.spaceInset) // // Calendar View containerView.addSubview(collectionView) @@ -220,7 +220,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } var indicatorCount = 0 - for x in (0...(self.indicators.count-1)) { + for x in (0...(self.indicators.count-1)) { if (self.days[indexPath.row] == self.getDay(with: self.indicators[x].date)) { indicatorCount += 1 } @@ -235,7 +235,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { return UICollectionReusableView() } - header.configure(with: true) + header.configure(with: surface) return header } else { // Footer @@ -243,7 +243,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { return UICollectionReusableView() } - footer.configure(with: indicators) + footer.configure(with: surface, indicators: indicators) return footer } } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index fd33611b..68917069 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -28,6 +28,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int) { addSubview(dateView) + dateView.surface = surface dateView.numberLabel.text = text dateView.indicatorCount = indicatorCount dateView.dateIndicators = indicators @@ -120,7 +121,7 @@ private class DateView : View { // Indicators containerView.addSubview(stackView) - let topPos = containerSize.height * 0.6 + let topPos = containerSize.height * 0.7 stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo().pinCenterY() stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } diff --git a/VDS/Components/Calendar/CalendarFooterView.swift b/VDS/Components/Calendar/CalendarFooterView.swift index 84d76cab..6574dc84 100644 --- a/VDS/Components/Calendar/CalendarFooterView.swift +++ b/VDS/Components/Calendar/CalendarFooterView.swift @@ -127,14 +127,14 @@ private class LegendCollectionViewCell: UICollectionViewCell { private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) - private var title = Label().with { + private var title: Label = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .left $0.numberOfLines = 1 $0.textStyle = .bodySmall - $0.backgroundColor = .clear $0.isAccessibilityElement = false } + private var legendIndicatorWrapper: View = View().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear @@ -180,6 +180,7 @@ private class LegendCollectionViewCell: UICollectionViewCell { } func updateTitle(text: String, color: UIColor, surface: Surface, clearFullcircle: Bool, drawSemiCircle: Bool) { + title.surface = surface title.text = text title.textColor = textColorConfiguration.getColor(surface) diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index 44f5462f..f217ae40 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -130,6 +130,7 @@ open class CalendarHeaderView: View { .pinLeading() .pinTrailing() .pinBottom(VDSLayout.space1X) + stackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() // month label view, previous and next buttons stackView.addArrangedSubview(monthView) @@ -157,11 +158,14 @@ open class CalendarHeaderView: View { stackView.addArrangedSubview(daysCollectionView) daysCollectionView.widthAnchor.constraint(equalTo: widthAnchor).activate() daysCollectionView.heightAnchor.constraint(equalTo: monthView.heightAnchor).activate() - print("daysOfWeek: \(daysOfWeek)") + daysCollectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() } open override func updateView() { super.updateView() + monthYearLabel.surface = surface + previousButton.surface = surface + nextButton.surface = surface daysCollectionView.reloadData() monthYearLabel.text = "May 2024" monthYearLabel.textColor = monthYearLabelTextColorConfiguration.getColor(surface) diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index 33bedfe2..3a61e635 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -24,7 +24,8 @@ class CalendarHeaderReusableView: UICollectionReusableView { fatalError("init(coder:) has not been implemented") } - func configure(with color: Bool) { + func configure(with surface: Surface) { + headerView.surface = surface addSubview(headerView) } @@ -49,8 +50,9 @@ class CalendarFooterReusableView: UICollectionReusableView { fatalError("init(coder:) has not been implemented") } - func configure(with indicators: [CalendarBase.CalendarIndicatorModel]) { + func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel]) { footerView.items = indicators + footerView.surface = surface addSubview(footerView) } From 3eaf29f3f1ec7f0c3ef482b99f00c683a4bf6d55 Mon Sep 17 00:00:00 2001 From: vasavk Date: Mon, 6 May 2024 09:47:02 +0530 Subject: [PATCH 13/87] Digital ACT-191 ONEAPP-7016 story: hide/show container border --- VDS/Components/Calendar/Calendar.swift | 31 ++++++++++++------- .../Calendar/CalendarReusableView.swift | 8 ----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 3e1442e1..41890756 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -33,7 +33,7 @@ open class CalendarBase: View { // MARK: - Public Properties //-------------------------------------------------- /// If set to true, the calendar will not have a border. - open var hideContainerBorder: Bool = false + open var hideContainerBorder: Bool = false { didSet { setNeedsUpdate() } } /// If set to true, the calendar will not have current date indication. open var hideCurrentDateIndicator: Bool = false @@ -123,7 +123,7 @@ open class CalendarBase: View { $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) } - + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -144,9 +144,8 @@ open class CalendarBase: View { .width(containerSize.width) containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - containerView.layer.borderWidth = VDSFormControls.borderWidth - - let spacing = CGFloat(VDSLayout.space2X) //CGFloat(VDSFormControls.spaceInset) // + + let spacing = CGFloat(VDSFormControls.spaceInset) //CGFloat(VDSLayout.space2X) // Calendar View containerView.addSubview(collectionView) @@ -166,7 +165,15 @@ open class CalendarBase: View { super.updateView() self.fetchDates(with: selectedDate ?? Date()) collectionView.reloadData() - containerView.layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor + if hideContainerBorder { + layer.borderColor = nil + layer.borderWidth = 0 + layer.cornerRadius = 0 + } else { + layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor + layer.borderWidth = VDSFormControls.borderWidth + layer.cornerRadius = VDSFormControls.borderRadius + } } override open func layoutSubviews() { @@ -220,9 +227,11 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } var indicatorCount = 0 - for x in (0...(self.indicators.count-1)) { - if (self.days[indexPath.row] == self.getDay(with: self.indicators[x].date)) { - indicatorCount += 1 + if self.indicators.count > 0 { + for x in (0...(self.indicators.count-1)) { + if (self.days[indexPath.row] == self.getDay(with: self.indicators[x].date)) { + indicatorCount += 1 + } } } cell.configure(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount) @@ -251,8 +260,8 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let date = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! -// print("selected day: \(days[indexPath.row]), date: \(date)") +// let selectedDate = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! +// print("selected day: \(days[indexPath.row]), date: \(selectedDate)") } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index 3a61e635..ebb2d269 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -28,10 +28,6 @@ class CalendarHeaderReusableView: UICollectionReusableView { headerView.surface = surface addSubview(headerView) } - - override func layoutSubviews() { - super.layoutSubviews() - } } /// Custom footer view @@ -55,8 +51,4 @@ class CalendarFooterReusableView: UICollectionReusableView { footerView.surface = surface addSubview(footerView) } - - override func layoutSubviews() { - super.layoutSubviews() - } } From e980d816b327feca616b095c89f5f65b9e38edfd Mon Sep 17 00:00:00 2001 From: vasavk Date: Mon, 6 May 2024 12:30:32 +0530 Subject: [PATCH 14/87] Digital ACT-191 ONEAPP-7016 story: applying selected/unselected properties and updating selected date on change --- VDS/Components/Calendar/Calendar.swift | 55 +++++++++-------- .../CalendarDateCollectionViewCell.swift | 59 +++++++++++++++---- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 41890756..cfd2c11d 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -54,15 +54,10 @@ open class CalendarBase: View { /// If provided, this is the date that will show as selected by the Calendar. /// If no value is provided, the current date will be used. If null is provided, no date will be selected. - open var selectedDate: Date? { + open var selectedDate: Date { get { return _selectedDate } set { - if let newValue { - _selectedDate = newValue - } else { - _selectedDate = Date() - } - //update views what needed + _selectedDate = newValue setNeedsUpdate() } } @@ -75,22 +70,25 @@ open class CalendarBase: View { // /// Array of indicators for the legend. // open var indicatorData: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - - /// A callback when the selected date changes.. - open var onChangeSelectedDate: ((Date) -> String)? + + /// A callback when the date changes. Passes parameters (selectedDate). + public var onChangeSelectedDate: ((Date) -> Void)? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var _selectedDate: Date = Date() - private var dates: [Date] = [] - private var days: [String] = [] internal var containerSize: CGSize { CGSize(width: 328, height: 376) } //width:320/328 + private let cellItemSize = CGSize(width: 40, height: 40) private let headerHeight = 88.0 private let footerHeight = 40.0 private let items = 35 + private var selectedIndexPath : IndexPath? + private var dates: [Date] = [] + private var days: [String] = [] + internal var containerView = View().with { $0.clipsToBounds = true } @@ -163,7 +161,7 @@ open class CalendarBase: View { open override func updateView() { super.updateView() - self.fetchDates(with: selectedDate ?? Date()) + self.fetchDates(with: selectedDate) collectionView.reloadData() if hideContainerBorder { layer.borderColor = nil @@ -189,15 +187,13 @@ open class CalendarBase: View { //-------------------------------------------------- func fetchDates(with date:Date) { days.removeAll() - if let dates = selectedDate?.calendarDisplayDays { - self.dates = dates - for date in dates { - // code to be executed - if date.monthInt != selectedDate?.monthInt { - days.append("") - } else { - days.append(getDay(with: date)) - } + self.dates = selectedDate.calendarDisplayDays + for date in dates { + // code to be executed + if date.monthInt != selectedDate.monthInt { + days.append("") + } else { + days.append(getDay(with: date)) } } } @@ -234,7 +230,8 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } } } - cell.configure(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount) + cell.configure(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate) + if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } return cell } @@ -260,8 +257,16 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { -// let selectedDate = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! -// print("selected day: \(days[indexPath.row]), date: \(selectedDate)") + let selectedItem = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! + onChangeSelectedDate?(selectedItem) + + selectedDate = self.dates[indexPath.row] + var reloadIndexPaths = [indexPath] + + // If an cell is already selected, then it needs to be deselected. + // Add its index path to the array of index paths to be reloaded. + if let deselectIndexPath = selectedIndexPath { reloadIndexPaths.append(deselectIndexPath) } + self.collectionView.reloadItems(at: reloadIndexPaths) } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index 68917069..eb66cde7 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -9,15 +9,20 @@ import Foundation import UIKit import VDSTokens -/// Calendar collection view cell +/// Calendar collection view for Date view final class CalendarDateCollectionViewCell: UICollectionViewCell { ///Identifier for the Calendar Date Cell static let identifier: String = String(describing: CalendarDateCollectionViewCell.self) - + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- private lazy var dateView = DateView() + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- override init(frame: CGRect) { super.init(frame: frame) } @@ -26,17 +31,15 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int) { + func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date) { addSubview(dateView) dateView.surface = surface dateView.numberLabel.text = text dateView.indicatorCount = indicatorCount dateView.dateIndicators = indicators + dateView.selectedDate = selectedDate } - override func layoutSubviews() { - super.layoutSubviews() - } } /// Date view to show Date number and indicator if applies @@ -67,9 +70,11 @@ private class DateView : View { open var numberLabel = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center - $0.textStyle = .bodySmall //isCurrentDate: .boldBodySmall + $0.textStyle = .bodySmall } + open var selectedDate: Date = Date() //? { didSet { setNeedsUpdate() } } + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -93,8 +98,15 @@ private class DateView : View { private lazy var shapeLayer = CAShapeLayer() - private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) + private let selectedBackgroundColor = SurfaceColorConfiguration(VDSColor.backgroundPrimaryInverseLight, VDSColor.backgroundPrimaryInverseDark) + private let selectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteGray65, VDSColor.paletteGray44) + + private let unselectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + private let unselectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + private let currentDate = Date() + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -130,17 +142,42 @@ private class DateView : View { super.updateView() numberLabel.surface = surface numberLabel.isEnabled = isEnabled - stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + // update text color, bg color, corner radius + if numberLabel.text == self.getDay(with: selectedDate) { + numberLabel.textColor = selectedTextColorConfiguration.getColor(self) + layer.backgroundColor = selectedBackgroundColor.getColor(self).cgColor + layer.cornerRadius = VDSFormControls.borderRadius + } else { + numberLabel.textColor = unselectedTextColorConfiguration.getColor(self) + layer.backgroundColor = nil + layer.cornerRadius = 0 + } + + // add indicators if indicatorCount > 0 { let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() for x in (0...(dateIndicators.count-1)) { if (self.numberLabel.text == self.getDay(with: dateIndicators[x].date)) { - addIndicator(with: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + if numberLabel.text == self.getDay(with: selectedDate) { + addIndicator(with: selectedCellIndicatorColorConfiguration.getColor(self), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + } else { + addIndicator(with: unselectedCellIndicatorColorConfiguration.getColor(self), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + } } } } + + // update text style for current date + if numberLabel.text == self.getDay(with: currentDate) { + numberLabel.textStyle = .boldBodySmall + } else { + numberLabel.textStyle = .bodySmall + } + + } override open func layoutSubviews() { @@ -167,7 +204,7 @@ private class DateView : View { // update indicator indicatorView.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) - indicatorView.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor + indicatorView.layer.borderColor = color.cgColor self.layoutIfNeeded() From 7fe5123a69b0478921a782717622672d652ef939 Mon Sep 17 00:00:00 2001 From: vasavk Date: Mon, 6 May 2024 16:01:56 +0530 Subject: [PATCH 15/87] Digital ACT-191 ONEAPP-7016 story: config container background, hide/show current date indicator --- VDS/Components/Calendar/Calendar.swift | 10 +- .../CalendarDateCollectionViewCell.swift | 131 +++++------------- 2 files changed, 39 insertions(+), 102 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index cfd2c11d..dd0f08eb 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -36,7 +36,7 @@ open class CalendarBase: View { open var hideContainerBorder: Bool = false { didSet { setNeedsUpdate() } } /// If set to true, the calendar will not have current date indication. - open var hideCurrentDateIndicator: Bool = false + open var hideCurrentDateIndicator: Bool = false { didSet { setNeedsUpdate() } } /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. /// All other dates will be inactive @@ -117,10 +117,7 @@ open class CalendarBase: View { // MARK: - Configuration //-------------------------------------------------- internal var containerBorderColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight , VDSColor.elementsPrimaryOndark) - internal var backgroundColorConfiguration = ControlColorConfiguration().with { - $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) - $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) - } + internal var backgroundColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) //-------------------------------------------------- // MARK: - Lifecycle @@ -163,6 +160,7 @@ open class CalendarBase: View { super.updateView() self.fetchDates(with: selectedDate) collectionView.reloadData() + layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor if hideContainerBorder { layer.borderColor = nil layer.borderWidth = 0 @@ -230,7 +228,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } } } - cell.configure(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate) + cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, hideDate: hideCurrentDateIndicator) if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } return cell } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index eb66cde7..d822c820 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -15,66 +15,6 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { ///Identifier for the Calendar Date Cell static let identifier: String = String(describing: CalendarDateCollectionViewCell.self) - //-------------------------------------------------- - // MARK: - Private Properties - //-------------------------------------------------- - private lazy var dateView = DateView() - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date) { - addSubview(dateView) - dateView.surface = surface - dateView.numberLabel.text = text - dateView.indicatorCount = indicatorCount - dateView.dateIndicators = indicators - dateView.selectedDate = selectedDate - } - -} - -/// Date view to show Date number and indicator if applies -private class DateView : View { - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - required public init() { - super.init(frame: .zero) - } - - public override init(frame: CGRect) { - super.init(frame: .zero) - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - } - - //-------------------------------------------------- - // MARK: - Public Properties - //-------------------------------------------------- - open var indicatorCount: Int = 0 - - open var dateIndicators: [CalendarBase.CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - - open var numberLabel = Label().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.textAlignment = .center - $0.textStyle = .bodySmall - } - - open var selectedDate: Date = Date() //? { didSet { setNeedsUpdate() } } - //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -95,7 +35,13 @@ private class DateView : View { $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) } }() - + + private var numberLabel = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.textStyle = .bodySmall + } + private lazy var shapeLayer = CAShapeLayer() private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) @@ -104,20 +50,25 @@ private class DateView : View { private let unselectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) private let unselectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) - - private let currentDate = Date() + private let currentDate = Date() + //-------------------------------------------------- - // MARK: - Lifecycle + // MARK: - Initializers //-------------------------------------------------- - open override func initialSetup() { - super.initialSetup() + override init(frame: CGRect) { + super.init(frame: frame) + setUp() } - open override func setup() { - super.setup() + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + private func setUp() { isAccessibilityElement = false - addSubview(containerView) + contentView.addSubview(containerView) containerView .pinTop() .pinBottom() @@ -130,27 +81,27 @@ private class DateView : View { // Number label containerView.addSubview(numberLabel) numberLabel.pinToSuperView() - + // Indicators containerView.addSubview(stackView) let topPos = containerSize.height * 0.7 stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo().pinCenterY() stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } - - open override func updateView() { - super.updateView() + + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, hideDate: Bool) { + numberLabel.text = text numberLabel.surface = surface - numberLabel.isEnabled = isEnabled +// numberLabel.isEnabled = isEnabled stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - + // update text color, bg color, corner radius if numberLabel.text == self.getDay(with: selectedDate) { - numberLabel.textColor = selectedTextColorConfiguration.getColor(self) - layer.backgroundColor = selectedBackgroundColor.getColor(self).cgColor + numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) + layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor layer.cornerRadius = VDSFormControls.borderRadius } else { - numberLabel.textColor = unselectedTextColorConfiguration.getColor(self) + numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface) layer.backgroundColor = nil layer.cornerRadius = 0 } @@ -159,34 +110,23 @@ private class DateView : View { if indicatorCount > 0 { let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() - for x in (0...(dateIndicators.count-1)) { - if (self.numberLabel.text == self.getDay(with: dateIndicators[x].date)) { + for x in (0...(indicators.count-1)) { + if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { if numberLabel.text == self.getDay(with: selectedDate) { - addIndicator(with: selectedCellIndicatorColorConfiguration.getColor(self), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + addIndicator(with: selectedCellIndicatorColorConfiguration.getColor(surface), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) } else { - addIndicator(with: unselectedCellIndicatorColorConfiguration.getColor(self), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + addIndicator(with: unselectedCellIndicatorColorConfiguration.getColor(surface), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) } } } } // update text style for current date - if numberLabel.text == self.getDay(with: currentDate) { - numberLabel.textStyle = .boldBodySmall + if (numberLabel.text == self.getDay(with: currentDate)) { + numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall } - - - } - - override open func layoutSubviews() { - super.layoutSubviews() - } - - open override func reset() { - super.reset() - numberLabel.textStyle = .bodySmall } //-------------------------------------------------- @@ -235,5 +175,4 @@ private class DateView : View { return day } } - } From 8560d7a6d4426fe51bca1659056b16cc47194bfc Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 7 May 2024 11:13:56 +0530 Subject: [PATCH 16/87] Digital ACT-191 ONEAPP-7016 story: handling active dates, inactive dates, min date, max date with updated values --- VDS/Components/Calendar/Calendar.swift | 32 +++------ .../CalendarDateCollectionViewCell.swift | 68 +++++++++++++------ .../Calendar/CalendarHeaderView.swift | 15 ++-- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index dd0f08eb..179f3efa 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -40,46 +40,36 @@ open class CalendarBase: View { /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. /// All other dates will be inactive - open var activeDates: [Date] = [] - + open var activeDates: [Date] = [] { didSet { setNeedsUpdate() } } + /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. /// All other dates will be active. - open var inactiveDates: [Date] = [] + open var inactiveDates: [Date] = [] { didSet { setNeedsUpdate() } } /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. - open var minDate: Date? - + open var minDate: Date = Date() { didSet { setNeedsUpdate() } } + /// If provided, the calendar will allow a selection to be made up to this date. - open var maxDate: Date? + open var maxDate: Date = Date() { didSet { setNeedsUpdate() } } /// If provided, this is the date that will show as selected by the Calendar. /// If no value is provided, the current date will be used. If null is provided, no date will be selected. - open var selectedDate: Date { - get { return _selectedDate } - set { - _selectedDate = newValue - setNeedsUpdate() - } - } + open var selectedDate: Date = Date() { didSet { setNeedsUpdate() } } /// If provided, the calendar will be rendered with transparent background. - open var transparentBackground: Bool = false + open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } } /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. open var indicators: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - -// /// Array of indicators for the legend. -// open var indicatorData: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - + /// A callback when the date changes. Passes parameters (selectedDate). public var onChangeSelectedDate: ((Date) -> Void)? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var _selectedDate: Date = Date() internal var containerSize: CGSize { CGSize(width: 328, height: 376) } //width:320/328 - + private let cellItemSize = CGSize(width: 40, height: 40) private let headerHeight = 88.0 private let footerHeight = 40.0 @@ -228,7 +218,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } } } - cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, hideDate: hideCurrentDateIndicator) + cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } return cell } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index d822c820..71562de5 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -9,7 +9,7 @@ import Foundation import UIKit import VDSTokens -/// Calendar collection view for Date view +/// Calendar collection view cell for Date view final class CalendarDateCollectionViewCell: UICollectionViewCell { ///Identifier for the Calendar Date Cell @@ -35,22 +35,23 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) } }() - + private var numberLabel = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center $0.textStyle = .bodySmall } - + private lazy var shapeLayer = CAShapeLayer() private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) private let selectedBackgroundColor = SurfaceColorConfiguration(VDSColor.backgroundPrimaryInverseLight, VDSColor.backgroundPrimaryInverseDark) private let selectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteGray65, VDSColor.paletteGray44) - private let unselectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) private let unselectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) - + private let disabledTextColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark) + private let disabledBackgroundColor = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) + private let currentDate = Date() //-------------------------------------------------- @@ -66,6 +67,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { setUp() } + /// Configuring the cell with default setup private func setUp() { isAccessibilityElement = false contentView.addSubview(containerView) @@ -81,22 +83,53 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // Number label containerView.addSubview(numberLabel) numberLabel.pinToSuperView() - + // Indicators containerView.addSubview(stackView) let topPos = containerSize.height * 0.7 stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo().pinCenterY() stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } - - func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, hideDate: Bool) { - numberLabel.text = text + + /// Updating UI based on selected date, modified indicators data along with surface + /// Enable/disable cell based on min date, max date, active dates, inactive dates + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { numberLabel.surface = surface -// numberLabel.isEnabled = isEnabled + numberLabel.text = text stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - + + // disabled cells based on min date, max date. + if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { + numberLabel.isEnabled = false + numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) + layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + } else { + numberLabel.isEnabled = false + // handing active dates + if activeDates.count > 0 && inactiveDates.count == 0 { + for x in (0...(activeDates.count-1)) { + if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { + numberLabel.isEnabled = true + } + } + } else { + numberLabel.isEnabled = true + } + } + + // handling inactive dates + if inactiveDates.count > 0 { + for x in (0...(inactiveDates.count-1)) { + if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { + numberLabel.isEnabled = false + numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) + layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + } + } + } + // update text color, bg color, corner radius - if numberLabel.text == self.getDay(with: selectedDate) { + if (numberLabel.text == self.getDay(with: selectedDate)) && numberLabel.isEnabled { numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor layer.cornerRadius = VDSFormControls.borderRadius @@ -112,18 +145,15 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() for x in (0...(indicators.count-1)) { if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { - if numberLabel.text == self.getDay(with: selectedDate) { - addIndicator(with: selectedCellIndicatorColorConfiguration.getColor(surface), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) - } else { - addIndicator(with: unselectedCellIndicatorColorConfiguration.getColor(surface), surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) - } + let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) + addIndicator(with: color, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) } } } // update text style for current date if (numberLabel.text == self.getDay(with: currentDate)) { - numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall + numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall } @@ -164,7 +194,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { indicatorView.layer.addSublayer(shapeLayer) } - func getDay(with date:Date) -> String { + func getDay(with date: Date) -> String { if #available(iOS 15.0, *) { return date.formatted(.dateTime.day()) } else { diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index f217ae40..c47f4df9 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -33,7 +33,7 @@ open class CalendarHeaderView: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 304, height: 88) } //width:320/328 + internal var containerSize: CGSize { CGSize(width: 304, height: 88) } private lazy var stackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -60,7 +60,7 @@ open class CalendarHeaderView: View { internal var nextMonthView = View() internal var daysView = View() internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays - + internal var previousButton = ButtonIcon().with { $0.kind = .ghost $0.iconName = .leftCaret @@ -112,7 +112,7 @@ open class CalendarHeaderView: View { super.setup() isAccessibilityElement = false - // stackView + // containerView addSubview(containerView) containerView .pinTop() @@ -127,9 +127,9 @@ open class CalendarHeaderView: View { containerView.addSubview(stackView) stackView .pinTop() + .pinBottom(VDSLayout.space1X) .pinLeading() .pinTrailing() - .pinBottom(VDSLayout.space1X) stackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() // month label view, previous and next buttons @@ -145,7 +145,9 @@ open class CalendarHeaderView: View { monthYearHeaderStackView.addArrangedSubview(previousMonthView) previousMonthView.widthAnchor.constraint(equalToConstant: 40).activate() previousMonthView.addSubview(previousButton) - + let spacing = VDSLayout.space1X + previousButton.pinTop(spacing).pinBottom(spacing).pinLeading(spacing).pinTrailing(spacing) + // month year label monthYearHeaderStackView.addArrangedSubview(monthYearLabel) @@ -153,6 +155,7 @@ open class CalendarHeaderView: View { monthYearHeaderStackView.addArrangedSubview(nextMonthView) nextMonthView.widthAnchor.constraint(equalToConstant: 40).activate() nextMonthView.addSubview(nextButton) + nextButton.pinTop(spacing).pinBottom(spacing).pinLeading(spacing).pinTrailing(spacing) // days Collection View stackView.addArrangedSubview(daysCollectionView) @@ -177,6 +180,7 @@ open class CalendarHeaderView: View { open override func reset() { super.reset() + monthYearLabel.textStyle = .boldBodySmall } } @@ -242,6 +246,5 @@ private class collectionViewCell: UICollectionViewCell { title.surface = surface title.text = text title.textColor = textColorConfiguration.getColor(surface) - title.backgroundColor = .clear } } From 8d415a72f1986b0549a3f563b0d9e0945a07ec92 Mon Sep 17 00:00:00 2001 From: vasavk Date: Wed, 8 May 2024 12:02:08 +0530 Subject: [PATCH 17/87] Digital ACT-191 ONEAPP-7958 story: Fixed issues in constraints for the component and updated to flexible width --- VDS/Components/Calendar/Calendar.swift | 14 ++- .../CalendarDateCollectionViewCell.swift | 7 +- .../Calendar/CalendarFooterView.swift | 2 +- .../Calendar/CalendarHeaderView.swift | 90 ++++++++----------- 4 files changed, 46 insertions(+), 67 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 179f3efa..f57b45be 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -68,11 +68,12 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 328, height: 376) } //width:320/328 + internal var containerSize: CGSize { CGSize(width: 328, height: 376) } private let cellItemSize = CGSize(width: 40, height: 40) private let headerHeight = 88.0 private let footerHeight = 40.0 + private let calendarWidth = 304.0 private let items = 35 private var selectedIndexPath : IndexPath? @@ -127,22 +128,19 @@ open class CalendarBase: View { .pinTrailingLessThanOrEqualTo() .height(containerSize.height) .width(containerSize.width) - containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - - let spacing = CGFloat(VDSFormControls.spaceInset) //CGFloat(VDSLayout.space2X) // Calendar View containerView.addSubview(collectionView) + let calendarHeight = containerSize.height - (2 * VDSLayout.space4X) + let spacing = (containerSize.width - calendarWidth) / 2 collectionView .pinTop(VDSLayout.space4X) .pinBottom(VDSLayout.space4X) .pinLeading(spacing) .pinTrailing(spacing) - let width = containerSize.width - (2 * spacing) - collectionView.widthAnchor.constraint(equalToConstant: width).activate() - let height = containerSize.height - (2 * VDSLayout.space4X) - collectionView.heightAnchor.constraint(equalToConstant: height).activate() + .width(calendarWidth) + .height(calendarHeight) collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index 71562de5..f73a1ae1 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -33,6 +33,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { $0.spacing = VDSLayout.space1X $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) + $0.backgroundColor = .clear } }() @@ -87,7 +88,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // Indicators containerView.addSubview(stackView) let topPos = containerSize.height * 0.7 - stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo().pinCenterY() + stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo() stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } @@ -141,8 +142,8 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // add indicators if indicatorCount > 0 { - let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) - stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() +// let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) +// stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() for x in (0...(indicators.count-1)) { if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) diff --git a/VDS/Components/Calendar/CalendarFooterView.swift b/VDS/Components/Calendar/CalendarFooterView.swift index 6574dc84..1ebd73be 100644 --- a/VDS/Components/Calendar/CalendarFooterView.swift +++ b/VDS/Components/Calendar/CalendarFooterView.swift @@ -35,7 +35,7 @@ open class CalendarFooterView: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 304, height: 40) } //width:320/328 + internal var containerSize: CGSize { CGSize(width: 304, height: 40) } internal var containerView = View().with { $0.clipsToBounds = true diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index c47f4df9..53facda5 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -43,7 +43,7 @@ open class CalendarHeaderView: View { $0.backgroundColor = .clear } - private lazy var monthYearHeaderStackView = UIStackView().with { + private lazy var topHeaderView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.distribution = .fill $0.spacing = VDSLayout.space1X @@ -51,15 +51,22 @@ open class CalendarHeaderView: View { $0.backgroundColor = .clear } - internal var containerView = View().with { - $0.clipsToBounds = true - $0.backgroundColor = .clear - } - internal var monthView = View() + private lazy var daysCollectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collectionView.isScrollEnabled = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.backgroundColor = .clear + collectionView.register(collectionViewCell.self, forCellWithReuseIdentifier: collectionViewCell.identifier) + return collectionView + }() + internal var previousMonthView = View() internal var nextMonthView = View() - internal var daysView = View() - internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays + let viewSize = 40.0 internal var previousButton = ButtonIcon().with { $0.kind = .ghost @@ -86,20 +93,8 @@ open class CalendarHeaderView: View { $0.isAccessibilityElement = false } - private lazy var daysCollectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - collectionView.isScrollEnabled = false - collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.delegate = self - collectionView.dataSource = self - collectionView.showsHorizontalScrollIndicator = false - collectionView.showsVerticalScrollIndicator = false - collectionView.backgroundColor = .clear - collectionView.register(collectionViewCell.self, forCellWithReuseIdentifier: collectionViewCell.identifier) - return collectionView - }() - - internal var monthYearLabelTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays + internal let monthYearLabelTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) //-------------------------------------------------- // MARK: - Lifecycle @@ -112,56 +107,41 @@ open class CalendarHeaderView: View { super.setup() isAccessibilityElement = false - // containerView - addSubview(containerView) - containerView - .pinTop() - .pinBottom() - .pinLeadingGreaterThanOrEqualTo() - .pinTrailingLessThanOrEqualTo() - .height(containerSize.height) - .width(containerSize.width) - containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - // stackview - containerView.addSubview(stackView) + addSubview(stackView) stackView .pinTop() .pinBottom(VDSLayout.space1X) .pinLeading() .pinTrailing() - stackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() + .height(containerSize.height) + .width(containerSize.width) + stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - // month label view, previous and next buttons - stackView.addArrangedSubview(monthView) - monthView.backgroundColor = .clear - monthView.heightAnchor.constraint(equalToConstant: 40).isActive = true - - // month Header stack view - monthView.addSubview(monthYearHeaderStackView) - monthYearHeaderStackView.pinToSuperView() + // top header stack view + stackView.addArrangedSubview(topHeaderView) + topHeaderView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true + topHeaderView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() // previous button - monthYearHeaderStackView.addArrangedSubview(previousMonthView) - previousMonthView.widthAnchor.constraint(equalToConstant: 40).activate() + topHeaderView.addArrangedSubview(previousMonthView) + previousMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() previousMonthView.addSubview(previousButton) - let spacing = VDSLayout.space1X - previousButton.pinTop(spacing).pinBottom(spacing).pinLeading(spacing).pinTrailing(spacing) + previousButton.pinCenterY().pinCenterX() // month year label - monthYearHeaderStackView.addArrangedSubview(monthYearLabel) + topHeaderView.addArrangedSubview(monthYearLabel) // next button - monthYearHeaderStackView.addArrangedSubview(nextMonthView) - nextMonthView.widthAnchor.constraint(equalToConstant: 40).activate() + topHeaderView.addArrangedSubview(nextMonthView) + nextMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() nextMonthView.addSubview(nextButton) - nextButton.pinTop(spacing).pinBottom(spacing).pinLeading(spacing).pinTrailing(spacing) + nextButton.pinCenterY().pinCenterX() // days Collection View stackView.addArrangedSubview(daysCollectionView) - daysCollectionView.widthAnchor.constraint(equalTo: widthAnchor).activate() - daysCollectionView.heightAnchor.constraint(equalTo: monthView.heightAnchor).activate() - daysCollectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() + topHeaderView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true + daysCollectionView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } open override func updateView() { @@ -197,7 +177,7 @@ extension CalendarHeaderView: UICollectionViewDelegate, UICollectionViewDataSour } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: 40, height: 40) + return CGSize(width: viewSize, height: viewSize) } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { From 80f90e0b56c735defb095c849a46c1c83264f510 Mon Sep 17 00:00:00 2001 From: vasavk Date: Wed, 8 May 2024 12:03:57 +0530 Subject: [PATCH 18/87] Digital ACT-191 ONEAPP-7958 story: added missing date function --- VDS/Components/Calendar/Date+Extension.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 07284507..13e80479 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -62,4 +62,7 @@ extension Date { Calendar.current.component(.month, from: self) } + var dayInt: Int { + Calendar.current.component(.day, from: self) + } } From c23d45f1aae57e9d62100e75c69a01fa6e4d6da7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 8 May 2024 11:03:23 -0500 Subject: [PATCH 19/87] updated version Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++-- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index a9804b9b..26df9645 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -1405,7 +1405,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 62; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1442,7 +1442,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 62; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 258951db..e7193230 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -2,6 +2,7 @@ ---------------- - CXTDT-546824 - Notification - Accessibility - Redundant text is provided for the notification icon. - CXTDT-553663 - DropdownSelect - Accessibility - 5 issues +- ONEAPP-6308 - Badge Indicator - Accessibility Text 1.0.61 ---------------- From 3134327dd5d8ef4e51edba88a71ae9000c44eee8 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 8 May 2024 15:25:51 -0500 Subject: [PATCH 20/87] fixed the tel type Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 00e7404f..4a78203f 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -433,6 +433,61 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } + // MARK: - Telephone + //--------------------------------------------------- + private func formatUSNumber(_ number: String) -> String { + // Format the number in the style XXX-XXX-XXXX + let areaCodeLength = 3 + let centralOfficeCodeLength = 3 + let lineNumberLength = 4 + + var formattedNumber = "" + + if number.count > 0 { + formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) + } + + if number.count > areaCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) + let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) + let lineNumber = number[startIndex.. UITextPosition? { + let start = range.location + let length = string.count + + let newCursorLocation = start + length + let rawNumberCount = rawNumber.count + + // Adjust the cursor position to skip over formatting characters + var formattedCharacterCount = 0 + for (index, character) in formattedNumber.enumerated() { + if index >= newCursorLocation + formattedCharacterCount { + break + } + if !character.isNumber { + formattedCharacterCount += 1 + } + } + + let finalCursorLocation = min(newCursorLocation + formattedCharacterCount, formattedNumber.count) + return textField.position(from: textField.beginningOfDocument, offset: finalCursorLocation) + } + } extension InputField: UITextFieldDelegate { @@ -457,13 +512,59 @@ extension InputField: UITextFieldDelegate { } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + +// case text, number, inlineAction, password, creditCard, tel, date, securityCode + switch fieldType { case .date: // Allow only numbers and limit the length of text. let allowedCharacters = CharacterSet.decimalDigits let characterSet = CharacterSet(charactersIn: string) return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= dateFormat.maxLength + + case .number: + // Allow only numbers + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + + case .tel: + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet(charactersIn: "01233456789") + let characterSet = CharacterSet(charactersIn: string) + let currentText = textField.text ?? "" + if !allowedCharacters.isSuperset(of: characterSet) { return false } + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + // Format the number with dashes + let formattedNumber = formatUSNumber(rawNumber) + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = getNewCursorPosition(textField: textField, + range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // Prevent the default behavior + return false + + case .securityCode: + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= 4 + default: return true } From 978db6482376ea5c11b4bc6c5336683e59e58dd1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 8 May 2024 15:59:43 -0500 Subject: [PATCH 21/87] updated rules Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 90 ++++++++++++++----- .../TextFields/Rules/CharacterCountRule.swift | 20 ++++- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 4a78203f..9a079000 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -280,17 +280,8 @@ open class InputField: EntryFieldBase { var actionModel: InputField.TextLinkModel? var toolTipModel: Tooltip.TooltipModel? = tooltipModel var isSecureTextEntry = false - var rules = [AnyRule]() var placeholderText: String? - if self.isRequired { - let rule = RequiredRule() - if let errorText { - rule.errorMessage = errorText - } - rules.append(.init(rule)) - } - switch fieldType { case .text: break @@ -329,7 +320,12 @@ open class InputField: EntryFieldBase { case .date: minWidth = 114.0 placeholderText = dateFormat.placeholderText - + let rule = CharacterCountRule().copyWith { + $0.maxLength = dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date" + } + rules.append(.init(rule)) case .securityCode: minWidth = 88.0 @@ -372,8 +368,26 @@ open class InputField: EntryFieldBase { //tooltip tooltipModel = toolTipModel + } + override func updateRules() { + super.updateRules() + + switch fieldType { + case .date: + let rule = CharacterCountRule().copyWith { + $0.maxLength = dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date" + } + rules.append(.init(rule)) + + default: break + + } + } + /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() @@ -433,6 +447,22 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } + private func formatDate(_ input: String) -> String { + var formattedInput = input.filter { $0.isNumber } // Remove any existing slashes + var formattedString = "" + var currentIndex = formattedInput.startIndex + + for index in 0.. String { @@ -466,12 +496,11 @@ open class InputField: EntryFieldBase { return formattedNumber } - private func getNewCursorPosition(textField: UITextField, range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { + private func getTelCursorPosition(textField: UITextField, range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { let start = range.location let length = string.count let newCursorLocation = start + length - let rawNumberCount = rawNumber.count // Adjust the cursor position to skip over formatting characters var formattedCharacterCount = 0 @@ -518,10 +547,24 @@ extension InputField: UITextFieldDelegate { switch fieldType { case .date: // Allow only numbers and limit the length of text. - let allowedCharacters = CharacterSet.decimalDigits - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= dateFormat.maxLength + guard let oldText = textField.text, + let textRange = Range(range, in: oldText), + string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil else { + return false + } + let newText = oldText.replacingCharacters(in: textRange, with: string) + if newText.count > dateFormat.maxLength { + return false + } + + if newText.count <= dateFormat.maxLength { + textField.text = formatDate(newText) + return false + } else { + return true + } + case .number: // Allow only numbers let allowedCharacters = CharacterSet.decimalDigits @@ -548,7 +591,7 @@ extension InputField: UITextFieldDelegate { textField.text = formattedNumber // Calculate the new cursor position - if let newPosition = getNewCursorPosition(textField: textField, + if let newPosition = getTelCursorPosition(textField: textField, range: range, replacementString: string, rawNumber: rawNumber, @@ -596,7 +639,7 @@ extension InputField { case mmyy case mmddyy case mmddyyyy - + public var placeholderText: String { switch self { case .mmyy: "MM/YY" @@ -604,7 +647,7 @@ extension InputField { case .mmddyyyy: "MM/DD/YYYY" } } - + public var formatString: String { switch self { case .mmyy: "MM/yy" @@ -612,7 +655,7 @@ extension InputField { case .mmddyyyy: "MM/dd/yyyy" } } - + public var maxLength: Int { switch self { case .mmyy: 5 @@ -620,6 +663,13 @@ extension InputField { case .mmddyyyy: 10 } } - + + internal var separatorIndices: [Int] { + switch self { + case .mmyy: [2] + case .mmddyy: [2,4] + case .mmddyyyy: [2,4] + } + } } } diff --git a/VDS/Components/TextFields/Rules/CharacterCountRule.swift b/VDS/Components/TextFields/Rules/CharacterCountRule.swift index f26ccafc..46cbed05 100644 --- a/VDS/Components/TextFields/Rules/CharacterCountRule.swift +++ b/VDS/Components/TextFields/Rules/CharacterCountRule.swift @@ -7,12 +7,28 @@ import Foundation -class CharacterCountRule: Rule { +class CharacterCountRule: Rule, Withable { + enum CompareType { + case equals, greaterThanEquals, lessThan, lessThanEquals + } var maxLength: Int? var errorMessage: String = "You have exceeded the character limit." + var compareType: CompareType = .lessThanEquals func isValid(value: String?) -> Bool { guard let text = value, let maxLength, maxLength > 0 else { return true } - return text.count <= maxLength + switch compareType { + case .equals: + return text.count == maxLength + + case .greaterThanEquals: + return text.count >= maxLength + + case .lessThan: + return text.count < maxLength + + case .lessThanEquals: + return text.count <= maxLength + } } } From 5d9b7440775cd27861f13b30586d7a0962e29dc4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 8 May 2024 16:08:33 -0500 Subject: [PATCH 22/87] added compare to date/tel type Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 9a079000..875405b9 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -375,14 +375,24 @@ open class InputField: EntryFieldBase { super.updateRules() switch fieldType { - case .date: - let rule = CharacterCountRule().copyWith { - $0.maxLength = dateFormat.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid date" + case .tel: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXX-XXX-XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid telephone." + } + rules.append(.init(rule)) + } + case .date: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date." + } + rules.append(.init(rule)) } - rules.append(.init(rule)) - default: break } @@ -448,7 +458,7 @@ open class InputField: EntryFieldBase { open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } private func formatDate(_ input: String) -> String { - var formattedInput = input.filter { $0.isNumber } // Remove any existing slashes + let formattedInput = input.filter { $0.isNumber } // Remove any existing slashes var formattedString = "" var currentIndex = formattedInput.startIndex From 819aa7a44e21f075d3a96f21ee8e6fec6b99e100 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 8 May 2024 17:00:41 -0500 Subject: [PATCH 23/87] refactored some of the credit card Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 136 ++++++++++++++---- 1 file changed, 111 insertions(+), 25 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 875405b9..cee206f7 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -94,7 +94,11 @@ open class InputField: EntryFieldBase { /// Value for the textField open override var value: String? { - textField.text + if fieldType == .creditCard { + return creditCardRawNumber + } else { + return textField.text + } } var _showError: Bool = false @@ -170,21 +174,29 @@ open class InputField: EntryFieldBase { .textPublisher .sink { [weak self] newText in print("textPublisher newText: \(newText)") - self?.process(text: newText) - self?.validate() self?.sendActions(for: .valueChanged) }.store(in: &subscribers) textField .publisher(for: .editingDidBegin) .sink { [weak self] _ in - self?.setNeedsUpdate() + guard let self else { return } + if self.fieldType == .creditCard { + self.isCreditCardMasked = false + self.textField.text = self.formatCreditCardNumber(self.creditCardRawNumber) + } + self.setNeedsUpdate() }.store(in: &subscribers) textField .publisher(for: .editingDidEnd) .sink { [weak self] _ in - self?.validate() + guard let self else { return } + if self.fieldType == .creditCard { + self.isCreditCardMasked = true + self.textField.text = self.maskCreditCardNumber(self.creditCardRawNumber) + } + self.validate() }.store(in: &subscribers) stackView.addArrangedSubview(successLabel) @@ -328,7 +340,7 @@ open class InputField: EntryFieldBase { rules.append(.init(rule)) case .securityCode: minWidth = 88.0 - + isSecureTextEntry = true } //textField @@ -375,6 +387,16 @@ open class InputField: EntryFieldBase { super.updateRules() switch fieldType { + case .creditCard: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXXX XXXX XXXX XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid credit card." + } + rules.append(.init(rule)) + } + case .tel: if let text = textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { @@ -473,6 +495,46 @@ open class InputField: EntryFieldBase { return formattedString } + //--------------------------------------------------- + // MARK: - Credit Card + //--------------------------------------------------- + private var isCreditCardMasked: Bool = false + private var creditCardRawNumber: String = "" + private var creditCardMaxLength = 16 + + private func formatCreditCardNumber(_ number: String) -> String { + // Format the number in the style XXXX XXXX XXXX XXXX + var formattedNumber = "" + for (index, char) in number.enumerated() { + if index != 0 && index % 4 == 0 { + formattedNumber.append(" ") + } + formattedNumber.append(char) + } + + return formattedNumber + } + + private func updateCardTypeIcon(rawNumber: String) { +// let firstFourDigits = String(rawNumber.prefix(4)) +// if let icon = cardTypeIcons[firstFourDigits] { +// cardTypeIconView.image = icon +// } else { +// cardTypeIconView.image = nil +// } + } + + private func maskCreditCardNumber(_ number: String) -> String { + // Mask the first 12 characters if the length is 16 + let rawNumber = number.filter { $0.isNumber } + guard rawNumber.count == creditCardMaxLength else { return number } + let lastFourDigits = rawNumber.suffix(4) + let maskedSection = String(repeating: "•", count: 12) + let formattedMaskSection = formatCreditCardNumber(maskedSection) + return formattedMaskSection + " " + lastFourDigits + } + + //--------------------------------------------------- // MARK: - Telephone //--------------------------------------------------- private func formatUSNumber(_ number: String) -> String { @@ -530,31 +592,55 @@ open class InputField: EntryFieldBase { } extension InputField: UITextFieldDelegate { - public func process(text changedText: String) { - var newText: String = changedText - switch fieldType { - case .date: - guard newText.count <= dateFormat.maxLength else { return } - let numericText = newText.compactMap { $0.isNumber ? $0 : nil } - var formattedText = "" - - for (index, char) in numericText.enumerated() { - if (index == 2 || (index == 4 && (dateFormat != .mmyy))) && index < dateFormat.maxLength { - formattedText += "/" - } - formattedText.append(char) - } - newText = formattedText - - default: break - } - } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { // case text, number, inlineAction, password, creditCard, tel, date, securityCode switch fieldType { + case .creditCard: + let allowedCharacters = CharacterSet.decimalDigits + if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { + return false + } + + // Get the current text + let currentText = textField.text ?? "" + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + if rawNumber.count > creditCardMaxLength { + return false + } + + // Format the number with spaces + let formattedNumber = formatCreditCardNumber(rawNumber) + + // Update the icon based on the first four digits + updateCardTypeIcon(rawNumber: rawNumber) + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = getTelCursorPosition(textField: textField, + range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // if all passes, then set the number1 + creditCardRawNumber = rawNumber + + // Prevent the default behavior + return false + case .date: // Allow only numbers and limit the length of text. guard let oldText = textField.text, From 2d5f3d765269482aa8bf007b0666a0bc66d99d28 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 8 May 2024 17:18:50 -0500 Subject: [PATCH 24/87] refactored more of inputfield Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index cee206f7..68959f82 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -177,28 +177,6 @@ open class InputField: EntryFieldBase { self?.sendActions(for: .valueChanged) }.store(in: &subscribers) - textField - .publisher(for: .editingDidBegin) - .sink { [weak self] _ in - guard let self else { return } - if self.fieldType == .creditCard { - self.isCreditCardMasked = false - self.textField.text = self.formatCreditCardNumber(self.creditCardRawNumber) - } - self.setNeedsUpdate() - }.store(in: &subscribers) - - textField - .publisher(for: .editingDidEnd) - .sink { [weak self] _ in - guard let self else { return } - if self.fieldType == .creditCard { - self.isCreditCardMasked = true - self.textField.text = self.maskCreditCardNumber(self.creditCardRawNumber) - } - self.validate() - }.store(in: &subscribers) - stackView.addArrangedSubview(successLabel) stackView.setCustomSpacing(8, after: successLabel) @@ -332,12 +310,6 @@ open class InputField: EntryFieldBase { case .date: minWidth = 114.0 placeholderText = dateFormat.placeholderText - let rule = CharacterCountRule().copyWith { - $0.maxLength = dateFormat.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid date" - } - rules.append(.init(rule)) case .securityCode: minWidth = 88.0 isSecureTextEntry = true @@ -390,7 +362,7 @@ open class InputField: EntryFieldBase { case .creditCard: if let text = textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { - $0.maxLength = "XXXX XXXX XXXX XXXX".count + $0.maxLength = creditCardMaxLength $0.compareType = .equals $0.errorMessage = "Enter a valid credit card." } @@ -498,7 +470,6 @@ open class InputField: EntryFieldBase { //--------------------------------------------------- // MARK: - Credit Card //--------------------------------------------------- - private var isCreditCardMasked: Bool = false private var creditCardRawNumber: String = "" private var creditCardMaxLength = 16 @@ -527,7 +498,7 @@ open class InputField: EntryFieldBase { private func maskCreditCardNumber(_ number: String) -> String { // Mask the first 12 characters if the length is 16 let rawNumber = number.filter { $0.isNumber } - guard rawNumber.count == creditCardMaxLength else { return number } + guard rawNumber.count == creditCardMaxLength else { return formatCreditCardNumber(number) } let lastFourDigits = rawNumber.suffix(4) let maskedSection = String(repeating: "•", count: 12) let formattedMaskSection = formatCreditCardNumber(maskedSection) @@ -592,6 +563,18 @@ open class InputField: EntryFieldBase { } extension InputField: UITextFieldDelegate { + public func textFieldDidBeginEditing(_ textField: UITextField) { + if fieldType == .creditCard { + textField.text = formatCreditCardNumber(creditCardRawNumber) + } + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + if self.fieldType == .creditCard { + textField.text = maskCreditCardNumber(creditCardRawNumber) + } + validate() + } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { From 450dd1aae8f6847e108c2da1174952e6e4def50d Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 9 May 2024 09:43:13 +0530 Subject: [PATCH 25/87] Digital ACT-191 ONEAPP-7958 story: added action for next and previous on changing min / max dates --- VDS/Components/Calendar/Calendar.swift | 52 +++++++++-- .../CalendarDateCollectionViewCell.swift | 22 ++--- .../Calendar/CalendarHeaderView.swift | 87 ++++++++++--------- .../Calendar/CalendarReusableView.swift | 22 ----- VDS/Components/Calendar/Date+Extension.swift | 36 ++++++++ 5 files changed, 139 insertions(+), 80 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index f57b45be..ce183fac 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -79,7 +79,8 @@ open class CalendarBase: View { private var selectedIndexPath : IndexPath? private var dates: [Date] = [] private var days: [String] = [] - + private var displayDate: Date = Date() + internal var containerView = View().with { $0.clipsToBounds = true } @@ -146,8 +147,14 @@ open class CalendarBase: View { open override func updateView() { super.updateView() - self.fetchDates(with: selectedDate) - collectionView.reloadData() + // range check between min & max dates + if (minDate <= maxDate) { + // Check if current date falls between min & max dates. + let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate) + displayDate = fallsBetween ? displayDate : minDate + self.fetchDates(with: displayDate) + collectionView.reloadData() + } layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor if hideContainerBorder { layer.borderColor = nil @@ -171,12 +178,12 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- - func fetchDates(with date:Date) { + func fetchDates(with aDate:Date) { days.removeAll() - self.dates = selectedDate.calendarDisplayDays + self.dates = aDate.calendarDisplayDays for date in dates { // code to be executed - if date.monthInt != selectedDate.monthInt { + if date.monthInt != aDate.monthInt { days.append("") } else { days.append(getDay(with: date)) @@ -216,7 +223,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } } } - cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) + cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, displayDate: displayDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } return cell } @@ -227,7 +234,36 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { return UICollectionReusableView() } - header.configure(with: surface) + var nextEnabled = false + var prevEnabled = false + + // check the interval between min date, max date.. set enable/disable flag for next / previous buttons. + if ((displayDate.monthInt < maxDate.monthInt) && (displayDate.yearInt == maxDate.yearInt)) || (displayDate.yearInt < maxDate.yearInt) { + nextEnabled = true + } + if ((minDate.monthInt < displayDate.monthInt) && (minDate.yearInt == displayDate.yearInt)) || (minDate.yearInt < displayDate.yearInt) { + prevEnabled = true + } + + header.nextClicked = { [weak self] in + guard let self = self else { return } + let aDate = Calendar.current.date(byAdding: .month, value: 1, to:self.displayDate)! + if ((aDate.monthInt <= maxDate.monthInt) && (aDate.yearInt == maxDate.yearInt)) || (aDate.yearInt < maxDate.yearInt) { + displayDate = aDate + self.fetchDates(with: displayDate) + self.collectionView.reloadData() + } + } + header.previousClicked = { [weak self] in + guard let self = self else { return } + let aDate = Calendar.current.date(byAdding: .month, value: -1, to:self.displayDate)! + if ((minDate.monthInt <= aDate.monthInt) && (minDate.yearInt == aDate.yearInt)) || (minDate.yearInt < aDate.yearInt) { + displayDate = aDate + self.fetchDates(with: displayDate) + self.collectionView.reloadData() + } + } + header.update(with: surface, aDate: displayDate, nextEnabled: nextEnabled, previousEnabled: prevEnabled) return header } else { // Footer diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index f73a1ae1..efc506d8 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -94,12 +94,12 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { /// Updating UI based on selected date, modified indicators data along with surface /// Enable/disable cell based on min date, max date, active dates, inactive dates - func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, displayDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { numberLabel.surface = surface numberLabel.text = text stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - // disabled cells based on min date, max date. + + // enable/disable cells based on min date, max date. if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { numberLabel.isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) @@ -121,16 +121,18 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // handling inactive dates if inactiveDates.count > 0 { for x in (0...(inactiveDates.count-1)) { - if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { - numberLabel.isEnabled = false - numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) - layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + if (activeDates[x].monthInt == displayDate.monthInt) && (activeDates[x].yearInt == displayDate.yearInt) { + if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { + numberLabel.isEnabled = false + numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) + layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + } } } } // update text color, bg color, corner radius - if (numberLabel.text == self.getDay(with: selectedDate)) && numberLabel.isEnabled { + if (numberLabel.text == self.getDay(with: selectedDate)) && (selectedDate.monthInt == displayDate.monthInt) && (selectedDate.yearInt == displayDate.yearInt) && numberLabel.isEnabled { numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor layer.cornerRadius = VDSFormControls.borderRadius @@ -142,8 +144,6 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // add indicators if indicatorCount > 0 { -// let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) -// stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() for x in (0...(indicators.count-1)) { if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) @@ -153,7 +153,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { } // update text style for current date - if (numberLabel.text == self.getDay(with: currentDate)) { + if (numberLabel.text == self.getDay(with: currentDate)) && (currentDate.monthInt == displayDate.monthInt) { numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index 53facda5..cdb08a18 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -10,25 +10,19 @@ import UIKit import VDSTokens /// Header view to display month and year along with days of week -open class CalendarHeaderView: View { - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - required public init() { - super.init(frame: .zero) - } +class CalendarHeaderReusableView: UICollectionReusableView { - public override init(frame: CGRect) { - super.init(frame: .zero) - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - } + ///Identifier for the Calendar Header Reusable View + static let identifier: String = String(describing: CalendarHeaderReusableView.self) //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- + /// A callback when the next button clicked + public var nextClicked: (() -> (Void))? + + /// A callback when the previous button clicked + public var previousClicked: (() -> (Void))? //-------------------------------------------------- // MARK: - Private Properties @@ -64,6 +58,9 @@ open class CalendarHeaderView: View { return collectionView }() + private var surface: Surface = .light + private var displayDate: Date = Date() + internal var previousMonthView = View() internal var nextMonthView = View() let viewSize = 40.0 @@ -84,7 +81,7 @@ open class CalendarHeaderView: View { $0.size = .small } - internal var monthYearLabel = Label().with { + internal var headerTitle = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center $0.numberOfLines = 1 @@ -94,17 +91,23 @@ open class CalendarHeaderView: View { } internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays - internal let monthYearLabelTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) - + internal let headerTitleTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + //-------------------------------------------------- - // MARK: - Lifecycle + // MARK: - Initializers //-------------------------------------------------- - open override func initialSetup() { - super.initialSetup() + override init(frame: CGRect) { + super.init(frame: frame) + setUp() } - open override func setup() { - super.setup() + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + /// Configuring the cell with default setup + private func setUp() { isAccessibilityElement = false // stackview @@ -114,7 +117,7 @@ open class CalendarHeaderView: View { .pinBottom(VDSLayout.space1X) .pinLeading() .pinTrailing() - .height(containerSize.height) + .height(containerSize.height - VDSLayout.space1X) .width(containerSize.width) stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() @@ -128,43 +131,49 @@ open class CalendarHeaderView: View { previousMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() previousMonthView.addSubview(previousButton) previousButton.pinCenterY().pinCenterX() - + previousButton.onClick = { _ in self.previousButtonClick() } + // month year label - topHeaderView.addArrangedSubview(monthYearLabel) + topHeaderView.addArrangedSubview(headerTitle) // next button topHeaderView.addArrangedSubview(nextMonthView) nextMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() nextMonthView.addSubview(nextButton) nextButton.pinCenterY().pinCenterX() - + nextButton.onClick = { _ in self.nextButtonClick() } + // days Collection View stackView.addArrangedSubview(daysCollectionView) topHeaderView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true daysCollectionView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } - open override func updateView() { - super.updateView() - monthYearLabel.surface = surface + /// Updating UI based on next/previous clicks along with surface. + /// Updating UI to enable/disable the next & previous buttons, updating header title + func update(with surface: Surface, aDate: Date, nextEnabled: Bool, previousEnabled: Bool) { + self.surface = surface + headerTitle.surface = surface previousButton.surface = surface nextButton.surface = surface + nextButton.isEnabled = nextEnabled + previousButton.isEnabled = previousEnabled daysCollectionView.reloadData() - monthYearLabel.text = "May 2024" - monthYearLabel.textColor = monthYearLabelTextColorConfiguration.getColor(surface) + let labelText = aDate.getMonthName(date: aDate) + " \(aDate.yearInt)" + headerTitle.text = labelText + headerTitle.textColor = headerTitleTextColorConfiguration.getColor(surface) } - override open func layoutSubviews() { - super.layoutSubviews() + func nextButtonClick() { + nextClicked?() } - - open override func reset() { - super.reset() - monthYearLabel.textStyle = .boldBodySmall + + func previousButtonClick() { + previousClicked?() } } -extension CalendarHeaderView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +extension CalendarHeaderReusableView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return daysOfWeek.count @@ -172,7 +181,7 @@ extension CalendarHeaderView: UICollectionViewDelegate, UICollectionViewDataSour public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionViewCell.identifier, for: indexPath) as? collectionViewCell else { return UICollectionViewCell() } - cell.updateTitle(text: daysOfWeek[indexPath.row], surface: surface) + cell.updateTitle(text: daysOfWeek[indexPath.row], surface: self.surface) return cell } diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index ebb2d269..4a695ab0 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -8,28 +8,6 @@ import UIKit import VDSTokens -/// Custom header view -class CalendarHeaderReusableView: UICollectionReusableView { - - ///Identifier for the Calendar Header Reusable View - static let identifier: String = String(describing: CalendarHeaderReusableView.self) - - private lazy var headerView = CalendarHeaderView() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(with surface: Surface) { - headerView.surface = surface - addSubview(headerView) - } -} - /// Custom footer view class CalendarFooterReusableView: UICollectionReusableView { diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 13e80479..2a016ecb 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -8,7 +8,10 @@ import Foundation extension Date { + static var firstDayOfWeek = Calendar.current.firstWeekday + + /// Capitalizes the first letter of the day of the week static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current let weekdays = calendar.shortWeekdaySymbols @@ -19,6 +22,18 @@ extension Date { } } + /// Returns all month names + static var fullMonthNames: [String] { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + + return (1...12).compactMap { month in + dateFormatter.setLocalizedDateFormatFromTemplate("MMMM") + let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1)) + return date.map { dateFormatter.string(from: $0) } + } + } + var startOfMonth: Date { Calendar.current.dateInterval(of: .month, for: self)!.start } @@ -28,6 +43,7 @@ extension Date { return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! } + /// Get the number of days of the month var numberOfDaysInMonth: Int { Calendar.current.component(.day, from: endOfMonth) } @@ -41,6 +57,7 @@ extension Date { return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! } + /// Get the days of the month to display var calendarDisplayDays: [Date] { var days: [Date] = [] // Start with days from the previous month to fill the grid @@ -58,11 +75,30 @@ extension Date { return days } + /// Returns the year value of the given date + var yearInt: Int { + Calendar.current.component(.year, from: self) + } + + /// Returns the month value of the given date var monthInt: Int { Calendar.current.component(.month, from: self) } + /// Returns the day value of the given date var dayInt: Int { Calendar.current.component(.day, from: self) } + + /// Check if the date falls between the given dates + func isBetweeen(date date1: Date, andDate date2: Date) -> Bool { + return date1.compare(self) == self.compare(date2) + } + + /// Returns the month name of the given date + func getMonthName(date: Date) -> String { + let names = Date.fullMonthNames + return names[date.monthInt - 1] + } + } From 4b5c4a28b25cbf6b29b7004322d397164d29b735 Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 9 May 2024 13:38:02 +0530 Subject: [PATCH 26/87] Digital ACT-191 ONEAPP-7958 story: handling days of month based on min, max date and active, inactive dates selected. --- VDS/Components/Calendar/Calendar.swift | 7 +- .../CalendarDateCollectionViewCell.swift | 128 +++++++++++++----- 2 files changed, 102 insertions(+), 33 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index ce183fac..e2f101d4 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -39,7 +39,7 @@ open class CalendarBase: View { open var hideCurrentDateIndicator: Bool = false { didSet { setNeedsUpdate() } } /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. - /// All other dates will be inactive + /// All other dates will be inactive. open var activeDates: [Date] = [] { didSet { setNeedsUpdate() } } /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. @@ -112,12 +112,13 @@ open class CalendarBase: View { internal var backgroundColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) //-------------------------------------------------- - // MARK: - Lifecycle + // MARK: - Overrides //-------------------------------------------------- open override func initialSetup() { super.initialSetup() } + /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() isAccessibilityElement = false @@ -171,6 +172,7 @@ open class CalendarBase: View { super.layoutSubviews() } + /// Resets to default settings. open override func reset() { super.reset() } @@ -283,6 +285,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI onChangeSelectedDate?(selectedItem) selectedDate = self.dates[indexPath.row] + displayDate = selectedDate var reloadIndexPaths = [indexPath] // If an cell is already selected, then it needs to be deselected. diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index efc506d8..40ba8b82 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -68,6 +68,10 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { setUp() } + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + /// Configuring the cell with default setup private func setUp() { isAccessibilityElement = false @@ -95,37 +99,20 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { /// Updating UI based on selected date, modified indicators data along with surface /// Enable/disable cell based on min date, max date, active dates, inactive dates func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, displayDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { + + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } numberLabel.surface = surface numberLabel.text = text - stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - // enable/disable cells based on min date, max date. - if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { - numberLabel.isEnabled = false - numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) - layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor - } else { - numberLabel.isEnabled = false - // handing active dates - if activeDates.count > 0 && inactiveDates.count == 0 { - for x in (0...(activeDates.count-1)) { - if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { - numberLabel.isEnabled = true - } - } - } else { - numberLabel.isEnabled = true - } - } + + // enable/disable cells based on min date, max date and active/inactive dates + self.updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) // handling inactive dates if inactiveDates.count > 0 { for x in (0...(inactiveDates.count-1)) { - if (activeDates[x].monthInt == displayDate.monthInt) && (activeDates[x].yearInt == displayDate.yearInt) { + if (inactiveDates[x].monthInt == displayDate.monthInt) && (inactiveDates[x].yearInt == displayDate.yearInt) { if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { - numberLabel.isEnabled = false - numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) - layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + disableLabel(with: surface) } } } @@ -145,24 +132,103 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // add indicators if indicatorCount > 0 { for x in (0...(indicators.count-1)) { - if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { - let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) - addIndicator(with: color, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + // irrespective of month and year, if it needs to show indicators on every month - comment below first if condition + if (indicators[x].date.monthInt == displayDate.monthInt) && (indicators[x].date.yearInt == displayDate.yearInt) { + if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { + let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) + addIndicator(with: color, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) + } } } } // update text style for current date - if (numberLabel.text == self.getDay(with: currentDate)) && (currentDate.monthInt == displayDate.monthInt) { + if (numberLabel.text == self.getDay(with: currentDate)) && (currentDate.monthInt == displayDate.monthInt) && (currentDate.yearInt == displayDate.yearInt) { numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall } } - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- + func disableLabel(with surface: Surface) { + numberLabel.isEnabled = false + numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) + layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + } + + func showActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + for x in (0...(activeDates.count-1)) { + if (activeDates[x].monthInt == displayDate.monthInt) && (activeDates[x].yearInt == displayDate.yearInt ) { + if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { + numberLabel.isEnabled = true + } + } + } + } + + // handing active dates if exist, else enable numberLabel to display day + func handleActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + if activeDates.count > 0 && inactiveDates.count == 0 { + showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + numberLabel.isEnabled = true + } + } + + // enable all days if no active dates, handing active dates if exist + func enableAllDaysAndCheckActiveDates(with surface:Surface, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + if activeDates.count > 0 && inactiveDates.count == 0 { + disableLabel(with: surface) + showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + numberLabel.isEnabled = true + } + } + + // enable/disable cells based on min date, max date and active/inactive dates + func updateLabel(with surface: Surface, displayDate: Date, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { + if (minDate.yearInt == displayDate.yearInt) || (maxDate.yearInt == displayDate.yearInt) { + if (minDate.monthInt == displayDate.monthInt) && (maxDate.monthInt == displayDate.monthInt) { + // validate days to enable/disable + if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else if (minDate.monthInt == displayDate.monthInt) || (maxDate.monthInt == displayDate.monthInt) { + if (minDate.monthInt == displayDate.monthInt) { + // validate days to enable/disable + if let day:Int = Int(numberLabel.text), day < minDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + + } else if (maxDate.monthInt == displayDate.monthInt) { + // validate days to enable/disable + if let day:Int = Int(numberLabel.text), day > maxDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + + } + } else if ((minDate.monthInt < displayDate.monthInt) || (displayDate.monthInt < maxDate.monthInt)) { + // enable all days if no active dates + // handing active dates + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else { + // enable all days if no active dates + // handing active dates + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + + } + func addIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool) { // add indicator let indicatorView: View = View().with { From 5672df70706803fac7bf0f9557942f371d4598fc Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 08:19:39 -0500 Subject: [PATCH 27/87] initial refactor to split out FieldTypes Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 28 +++ .../InputField/FieldTypes/CreditCard.swift | 103 ++++++++ .../InputField/FieldTypes/Date.swift | 65 +++++ .../InputField/FieldTypes/FieldType.swift | 32 +++ .../InputField/FieldTypes/Password.swift | 20 ++ .../InputField/FieldTypes/Telephone.swift | 43 ++++ .../TextFields/InputField/InputField.swift | 238 ++++-------------- 7 files changed, 346 insertions(+), 183 deletions(-) create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/Date.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/Password.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 26df9645..0be691c8 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -141,6 +141,11 @@ EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FF0029424ACB00998C17 /* UIControl.swift */; }; EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFEB632A26473700C4C106 /* NSAttributedString.swift */; }; EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */; }; + EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C052BED000200BA39FA /* CreditCard.swift */; }; + EAC58C082BED002D00BA39FA /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C072BED002D00BA39FA /* Date.swift */; }; + EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C092BED004E00BA39FA /* FieldType.swift */; }; + EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0B2BED01D500BA39FA /* Telephone.swift */; }; + EAC58C0E2BED021600BA39FA /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0D2BED021600BA39FA /* Password.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -337,6 +342,11 @@ EAB5FF0029424ACB00998C17 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; EABFEB632A26473700C4C106 /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = ""; }; EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupTextColor.swift; sourceTree = ""; }; + EAC58C052BED000200BA39FA /* CreditCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCard.swift; sourceTree = ""; }; + EAC58C072BED002D00BA39FA /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + EAC58C092BED004E00BA39FA /* FieldType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldType.swift; sourceTree = ""; }; + EAC58C0B2BED01D500BA39FA /* Telephone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telephone.swift; sourceTree = ""; }; + EAC58C0D2BED021600BA39FA /* Password.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Password.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -875,6 +885,18 @@ path = Tooltip; sourceTree = ""; }; + EAC58C042BECFFEA00BA39FA /* FieldTypes */ = { + isa = PBXGroup; + children = ( + EAC58C092BED004E00BA39FA /* FieldType.swift */, + EAC58C052BED000200BA39FA /* CreditCard.swift */, + EAC58C072BED002D00BA39FA /* Date.swift */, + EAC58C0B2BED01D500BA39FA /* Telephone.swift */, + EAC58C0D2BED021600BA39FA /* Password.swift */, + ); + path = FieldTypes; + sourceTree = ""; + }; EAC9257E29119B5D00091998 /* TextLink */ = { isa = PBXGroup; children = ( @@ -907,6 +929,7 @@ EAC925862911C9DE00091998 /* InputField */ = { isa = PBXGroup; children = ( + EAC58C042BECFFEA00BA39FA /* FieldTypes */, EAC925872911C9DE00091998 /* InputField.swift */, EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */, EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */, @@ -1142,6 +1165,7 @@ EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */, EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */, + EAC58C082BED002D00BA39FA /* Date.swift in Sources */, EA985BF7296C665E00F2FF2E /* IconName.swift in Sources */, EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */, EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, @@ -1195,12 +1219,14 @@ EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */, EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */, EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, + EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */, EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */, EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, + EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */, EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */, @@ -1214,6 +1240,7 @@ 44604AD729CE196600E62B51 /* Line.swift in Sources */, 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */, EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */, + EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */, EA5E3058295105A40082B959 /* Tilelet.swift in Sources */, 186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */, EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */, @@ -1246,6 +1273,7 @@ EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */, EA3362302891EB4A0071C351 /* Font.swift in Sources */, + EAC58C0E2BED021600BA39FA /* Password.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift new file mode 100644 index 00000000..dc4aeaf2 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -0,0 +1,103 @@ +// +// CreditCard.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + enum CreditCardType: CaseIterable { + case generic + case visa + case mastercard + case amex + case discover + case dinersClub + case jcb + case chinaUnionPay + + var image: UIImage { + return BundleManager.shared.image(for: imageName)! + } + + var imageName: String { + var imageName: String = "generic" + switch self { + case .visa: imageName = "visa" + case .mastercard: imageName = "mastercard" + case .amex: imageName = "amex" + case .discover: imageName = "discover" + case .dinersClub: imageName = "dinersClub" + case .jcb: imageName = "jcb" + default: imageName = "generic" + } + return imageName + } + + internal var separatorIndices: [Int] { + switch self { + case .dinersClub: + return [4, 10] + default: + return [4, 8, 12] + } + } + + internal var maxLength: Int { + switch self { + case .dinersClub: return 14 + default: return 16 + } + } + + static func from(iin: Int) -> CreditCardType? { + switch iin { + case 4000...4999: + return .visa + case 5100...5599, 2221...2720: + return .mastercard + case 3400...3499, 3700...3799: + return .amex + case 6011, 6221...6229, 6440...6499, 6500...6599: + return .discover + case 3600...3699, 3800...3999: + return .dinersClub + case 3528...3589: + return .jcb + case 6200...6299, 6000...6010, 8100...8199: + return .chinaUnionPay + default: + return nil + } + } + } + + internal func formatCreditCardNumber(_ number: String) -> String { + let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes + return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") + } + + internal func updateCardTypeIcon(rawNumber: String) { + guard rawNumber.count >= 4, + let firstFourDigits = Int(String(rawNumber.prefix(4))), + let creditCardType = CreditCardType.from(iin: firstFourDigits) else { + leftImageView.image = BundleManager.shared.image(for: CreditCardType.generic.imageName) + creditCardType = .generic + return + } + self.creditCardType = creditCardType + } + + internal func maskCreditCardNumber(_ number: String) -> String { + // Mask the first 12 characters if the length is 16 + let rawNumber = number.filter { $0.isNumber } + guard rawNumber.count == creditCardType.maxLength else { return formatCreditCardNumber(number) } + let lastFourDigits = rawNumber.suffix(4) + let maskedSection = String(repeating: "•", count: 12) + let formattedMaskSection = String.format(maskedSection, indices: creditCardType.separatorIndices, with: " ") + return formattedMaskSection + " " + lastFourDigits + } +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift new file mode 100644 index 00000000..a8ef3d78 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -0,0 +1,65 @@ +// +// Date.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation + +extension InputField { + public enum DateFormat: String, CaseIterable { + case mmyy + case mmddyy + case mmddyyyy + + public var placeholderText: String { + switch self { + case .mmyy: "MM/YY" + case .mmddyy: "MM/DD/YY" + case .mmddyyyy: "MM/DD/YYYY" + } + } + + public var formatString: String { + switch self { + case .mmyy: "MM/yy" + case .mmddyy: "MM/dd/yy" + case .mmddyyyy: "MM/dd/yyyy" + } + } + + public var maxLength: Int { + switch self { + case .mmyy: 5 + case .mmddyy: 8 + case .mmddyyyy: 10 + } + } + + internal var separatorIndices: [Int] { + switch self { + case .mmyy: [2] + case .mmddyy: [2,4] + case .mmddyyyy: [2,4] + } + } + } + + internal func formatDate(_ input: String) -> String { + let formattedInput = input.filter { $0.isNumber } // Remove any existing slashes + var formattedString = "" + var currentIndex = formattedInput.startIndex + + for index in 0.. PasswordAction { + self == .hide ? .show : .hide + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift new file mode 100644 index 00000000..3a13bc84 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -0,0 +1,43 @@ +// +// Tel.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation + +extension InputField { + + internal func formatUSNumber(_ number: String) -> String { + // Format the number in the style XXX-XXX-XXXX + let areaCodeLength = 3 + let centralOfficeCodeLength = 3 + let lineNumberLength = 4 + + var formattedNumber = "" + + if number.count > 0 { + formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) + } + + if number.count > areaCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) + let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) + let lineNumber = number[startIndex.. 0 { let rule = CharacterCountRule().copyWith { - $0.maxLength = creditCardMaxLength + $0.maxLength = creditCardType.maxLength $0.compareType = .equals $0.errorMessage = "Enter a valid credit card." } @@ -431,115 +423,9 @@ open class InputField: EntryFieldBase { } //-------------------------------------------------- - // MARK: - Password + // MARK: - Private Methods //-------------------------------------------------- - enum PasswordAction { - case show, hide - - func toggle() -> PasswordAction { - self == .hide ? .show : .hide - } - } - - internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } - - open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } - open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } - - //-------------------------------------------------- - // MARK: - Date - //-------------------------------------------------- - open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } - - private func formatDate(_ input: String) -> String { - let formattedInput = input.filter { $0.isNumber } // Remove any existing slashes - var formattedString = "" - var currentIndex = formattedInput.startIndex - - for index in 0.. String { - // Format the number in the style XXXX XXXX XXXX XXXX - var formattedNumber = "" - for (index, char) in number.enumerated() { - if index != 0 && index % 4 == 0 { - formattedNumber.append(" ") - } - formattedNumber.append(char) - } - - return formattedNumber - } - - private func updateCardTypeIcon(rawNumber: String) { -// let firstFourDigits = String(rawNumber.prefix(4)) -// if let icon = cardTypeIcons[firstFourDigits] { -// cardTypeIconView.image = icon -// } else { -// cardTypeIconView.image = nil -// } - } - - private func maskCreditCardNumber(_ number: String) -> String { - // Mask the first 12 characters if the length is 16 - let rawNumber = number.filter { $0.isNumber } - guard rawNumber.count == creditCardMaxLength else { return formatCreditCardNumber(number) } - let lastFourDigits = rawNumber.suffix(4) - let maskedSection = String(repeating: "•", count: 12) - let formattedMaskSection = formatCreditCardNumber(maskedSection) - return formattedMaskSection + " " + lastFourDigits - } - - //--------------------------------------------------- - // MARK: - Telephone - //--------------------------------------------------- - private func formatUSNumber(_ number: String) -> String { - // Format the number in the style XXX-XXX-XXXX - let areaCodeLength = 3 - let centralOfficeCodeLength = 3 - let lineNumberLength = 4 - - var formattedNumber = "" - - if number.count > 0 { - formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) - } - - if number.count > areaCodeLength { - let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) - let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) - let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { - let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) - let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) - let lineNumber = number[startIndex.. UITextPosition? { + internal func cursorPosition(textField: UITextField, range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { let start = range.location let length = string.count @@ -560,6 +446,28 @@ open class InputField: EntryFieldBase { return textField.position(from: textField.beginningOfDocument, offset: finalCursorLocation) } + //-------------------------------------------------- + // MARK: - Password + //-------------------------------------------------- + internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } + open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } + open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Date + //-------------------------------------------------- + open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } + + //--------------------------------------------------- + // MARK: - Credit Card + //--------------------------------------------------- + internal var creditCardRawNumber: String = "" + internal var creditCardType: CreditCardType = .generic { didSet { setNeedsUpdate() } } + + //--------------------------------------------------- + // MARK: - Telephone + //--------------------------------------------------- + } extension InputField: UITextFieldDelegate { @@ -596,7 +504,7 @@ extension InputField: UITextFieldDelegate { // Remove any existing formatting let rawNumber = newText.filter { $0.isNumber } - if rawNumber.count > creditCardMaxLength { + if rawNumber.count > creditCardType.maxLength { return false } @@ -606,11 +514,16 @@ extension InputField: UITextFieldDelegate { // Update the icon based on the first four digits updateCardTypeIcon(rawNumber: rawNumber) + // Check again + if rawNumber.count > creditCardType.maxLength { + return false + } + // Set the formatted text textField.text = formattedNumber // Calculate the new cursor position - if let newPosition = getTelCursorPosition(textField: textField, + if let newPosition = cursorPosition(textField: textField, range: range, replacementString: string, rawNumber: rawNumber, @@ -670,7 +583,7 @@ extension InputField: UITextFieldDelegate { textField.text = formattedNumber // Calculate the new cursor position - if let newPosition = getTelCursorPosition(textField: textField, + if let newPosition = cursorPosition(textField: textField, range: range, replacementString: string, rawNumber: rawNumber, @@ -693,62 +606,21 @@ extension InputField: UITextFieldDelegate { } } -extension InputField.FieldType { +extension String { - public var keyboardType: UIKeyboardType { - switch self { - case .number: - .numberPad - case .tel: - .phonePad - case .creditCard: - .numberPad - case .date: - .numberPad - case .securityCode: - .numberPad - default: - .default - } - } -} - -extension InputField { - public enum DateFormat: String, CaseIterable { - case mmyy - case mmddyy - case mmddyyyy - - public var placeholderText: String { - switch self { - case .mmyy: "MM/YY" - case .mmddyy: "MM/DD/YY" - case .mmddyyyy: "MM/DD/YYYY" - } - } - - public var formatString: String { - switch self { - case .mmyy: "MM/yy" - case .mmddyy: "MM/dd/yy" - case .mmddyyyy: "MM/dd/yyyy" - } - } - - public var maxLength: Int { - switch self { - case .mmyy: 5 - case .mmddyy: 8 - case .mmddyyyy: 10 - } - } - - internal var separatorIndices: [Int] { - switch self { - case .mmyy: [2] - case .mmddyy: [2,4] - case .mmddyyyy: [2,4] - } + internal static func format(_ value: String, indices: [Int], with separator: String) -> String { + var formattedString = "" + var currentIndex = value.startIndex + + for index in 0.. Date: Thu, 9 May 2024 08:30:19 -0500 Subject: [PATCH 28/87] refactored out more code into enums Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/FieldType.swift | 39 ++++++++++++++++++- .../TextFields/InputField/InputField.swift | 36 +---------------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index 2d6eebf4..8bb4f944 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -9,10 +9,11 @@ import Foundation import UIKit extension InputField { + public enum FieldType: String, CaseIterable { case text, number, inlineAction, password, creditCard, tel, date, securityCode - public var keyboardType: UIKeyboardType { + internal var keyboardType: UIKeyboardType { switch self { case .number: .numberPad @@ -28,5 +29,41 @@ extension InputField { .default } } + + internal func appendRules(for textField: InputField) { + switch self { + case .creditCard: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = textField.creditCardType.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid credit card." + } + textField.rules.append(.init(rule)) + } + + case .tel: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXX-XXX-XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid telephone." + } + textField.rules.append(.init(rule)) + } + case .date: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = textField.dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date." + } + textField.rules.append(.init(rule)) + } + default: break + + } + + } } } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index b221ac02..d96cfee4 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -312,7 +312,7 @@ open class InputField: EntryFieldBase { //leftIcon if let leftImageName { - leftImageView.image = BundleManager.shared.image(for: creditCardType.imageName)?.withTintColor(iconColorConfiguration.getColor(self)) + leftImageView.image = BundleManager.shared.image(for: leftImageName)?.withTintColor(iconColorConfiguration.getColor(self)) } leftImageView.isHidden = leftImageName == nil @@ -349,39 +349,7 @@ open class InputField: EntryFieldBase { override func updateRules() { super.updateRules() - - switch fieldType { - case .creditCard: - if let text = textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = creditCardType.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid credit card." - } - rules.append(.init(rule)) - } - - case .tel: - if let text = textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = "XXX-XXX-XXXX".count - $0.compareType = .equals - $0.errorMessage = "Enter a valid telephone." - } - rules.append(.init(rule)) - } - case .date: - if let text = textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = dateFormat.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid date." - } - rules.append(.init(rule)) - } - default: break - - } + fieldType.appendRules(for: self) } /// Used to update any Accessibility properties. From eab8e72785c9f7c75b75c46aa62bb5ef072020b8 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 08:57:03 -0500 Subject: [PATCH 29/87] refactor into FieldHandlers Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/FieldType.swift | 209 ++++++++++++++---- .../TextFields/InputField/InputField.swift | 4 +- 2 files changed, 168 insertions(+), 45 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index 8bb4f944..db09b3ca 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -9,61 +9,184 @@ import Foundation import UIKit extension InputField { + protocol FieldHandler: UITextFieldDelegate { + var keyboardType: UIKeyboardType { get } + func configure(for inputField: InputField) + func appendRules(for inputField: InputField) + } public enum FieldType: String, CaseIterable { - case text, number, inlineAction, password, creditCard, tel, date, securityCode + case text, number, inlineAction, password, creditCard, telephone, date, securityCode - internal var keyboardType: UIKeyboardType { + func handler() -> FieldHandler { switch self { + case .text: + return TextHandler.shared case .number: - .numberPad - case .tel: - .phonePad + return NumberHandler.shared + case .inlineAction: + return InlineActionHandler.shared + case .password: + return PasswordHandler.shared case .creditCard: - .numberPad + return CreditCardHandler.shared + case .telephone: + return TelephoneHandler.shared case .date: - .numberPad + return DateHandler.shared case .securityCode: - .numberPad - default: - .default + return SecurityCodeHandler.shared } } - internal func appendRules(for textField: InputField) { - switch self { - case .creditCard: - if let text = textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = textField.creditCardType.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid credit card." - } - textField.rules.append(.init(rule)) - } - - case .tel: - if let text = textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = "XXX-XXX-XXXX".count - $0.compareType = .equals - $0.errorMessage = "Enter a valid telephone." - } - textField.rules.append(.init(rule)) - } - case .date: - if let text = textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = textField.dateFormat.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid date." - } - textField.rules.append(.init(rule)) - } - default: break - - } + internal var keyboardType: UIKeyboardType { + handler().keyboardType + } + internal func appendRules(for textField: InputField) { + handler().appendRules(for: textField) } } } + +extension InputField { + class TextHandler: NSObject, FieldHandler { + static let shared = TextHandler() + + var keyboardType: UIKeyboardType { .default } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + + class NumberHandler: NSObject, FieldHandler { + static let shared = NumberHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + + class InlineActionHandler: NSObject, FieldHandler { + static let shared = InlineActionHandler() + + var keyboardType: UIKeyboardType { .default } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + + class PasswordHandler: NSObject, FieldHandler { + static let shared = PasswordHandler() + + var keyboardType: UIKeyboardType { .default } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + + class CreditCardHandler: NSObject, FieldHandler { + static let shared = CreditCardHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.creditCardType.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid credit card." + } + inputField.rules.append(.init(rule)) + } + } + } + + class TelephoneHandler: NSObject, FieldHandler { + static let shared = TelephoneHandler() + + var keyboardType: UIKeyboardType { .phonePad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXX-XXX-XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid telephone." + } + inputField.rules.append(.init(rule)) + } + } + } + + class DateHandler: NSObject, FieldHandler { + static let shared = DateHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date." + } + inputField.rules.append(.init(rule)) + } + } + } + + class SecurityCodeHandler: NSObject, FieldHandler { + static let shared = SecurityCodeHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + +} diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index d96cfee4..ccb7820c 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -296,7 +296,7 @@ open class InputField: EntryFieldBase { case .creditCard: minWidth = 288.0 leftImageName = creditCardType.imageName - case .tel: + case .telephone: minWidth = 176.0 case .date: @@ -531,7 +531,7 @@ extension InputField: UITextFieldDelegate { let characterSet = CharacterSet(charactersIn: string) return allowedCharacters.isSuperset(of: characterSet) - case .tel: + case .telephone: // Allow only numbers and limit the length of text. let allowedCharacters = CharacterSet(charactersIn: "01233456789") let characterSet = CharacterSet(charactersIn: string) From 14c578edbdebc13f2ccf3058acd98b5c1af5fb8f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 09:10:46 -0500 Subject: [PATCH 30/87] refactored into FieldTypeHandlers first cut Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 18 ++- .../InputField/FieldTypes/CreditCard.swift | 23 +++ .../InputField/FieldTypes/Date.swift | 25 +++ .../InputField/FieldTypes/FieldType.swift | 149 +----------------- .../InputField/FieldTypes/InlineAction.swift | 27 ++++ .../InputField/FieldTypes/Number.swift | 27 ++++ .../InputField/FieldTypes/Password.swift | 15 ++ .../InputField/FieldTypes/SecurityCode.swift | 27 ++++ .../InputField/FieldTypes/Telephone.swift | 24 +++ .../InputField/FieldTypes/Text.swift | 25 +++ .../TextFields/InputField/InputField.swift | 56 +++---- 11 files changed, 238 insertions(+), 178 deletions(-) create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/Number.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift create mode 100644 VDS/Components/TextFields/InputField/FieldTypes/Text.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 0be691c8..c60f125a 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -146,6 +146,10 @@ EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C092BED004E00BA39FA /* FieldType.swift */; }; EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0B2BED01D500BA39FA /* Telephone.swift */; }; EAC58C0E2BED021600BA39FA /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0D2BED021600BA39FA /* Password.swift */; }; + EAC58C122BED0DDD00BA39FA /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C112BED0DDD00BA39FA /* Text.swift */; }; + EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C132BED0DEC00BA39FA /* Number.swift */; }; + EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C152BED0E0300BA39FA /* InlineAction.swift */; }; + EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -347,6 +351,10 @@ EAC58C092BED004E00BA39FA /* FieldType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldType.swift; sourceTree = ""; }; EAC58C0B2BED01D500BA39FA /* Telephone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telephone.swift; sourceTree = ""; }; EAC58C0D2BED021600BA39FA /* Password.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Password.swift; sourceTree = ""; }; + EAC58C112BED0DDD00BA39FA /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; + EAC58C132BED0DEC00BA39FA /* Number.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Number.swift; sourceTree = ""; }; + EAC58C152BED0E0300BA39FA /* InlineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAction.swift; sourceTree = ""; }; + EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -891,8 +899,12 @@ EAC58C092BED004E00BA39FA /* FieldType.swift */, EAC58C052BED000200BA39FA /* CreditCard.swift */, EAC58C072BED002D00BA39FA /* Date.swift */, - EAC58C0B2BED01D500BA39FA /* Telephone.swift */, + EAC58C152BED0E0300BA39FA /* InlineAction.swift */, + EAC58C132BED0DEC00BA39FA /* Number.swift */, EAC58C0D2BED021600BA39FA /* Password.swift */, + EAC58C172BED0E2300BA39FA /* SecurityCode.swift */, + EAC58C0B2BED01D500BA39FA /* Telephone.swift */, + EAC58C112BED0DDD00BA39FA /* Text.swift */, ); path = FieldTypes; sourceTree = ""; @@ -1224,6 +1236,7 @@ EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, + EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, @@ -1263,6 +1276,7 @@ EA985BF5296C60C000F2FF2E /* Icon.swift in Sources */, EA3361AA288B25E40071C351 /* Disabling.swift in Sources */, EA3361B6288B2A410071C351 /* Control.swift in Sources */, + EAC58C122BED0DDD00BA39FA /* Text.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */, @@ -1279,7 +1293,9 @@ EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */, + EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */, EAC9257D29119B5400091998 /* TextLink.swift in Sources */, + EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */, EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */, EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */, diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index dc4aeaf2..10dd99c4 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -75,6 +75,29 @@ extension InputField { } } + class CreditCardHandler: NSObject, FieldTypeHandler { + static let shared = CreditCardHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.creditCardType.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid credit card." + } + inputField.rules.append(.init(rule)) + } + } + } + internal func formatCreditCardNumber(_ number: String) -> String { let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift index a8ef3d78..44dba3ac 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -6,8 +6,33 @@ // import Foundation +import UIKit extension InputField { + + class DateHandler: NSObject, FieldTypeHandler { + static let shared = DateHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date." + } + inputField.rules.append(.init(rule)) + } + } + } + public enum DateFormat: String, CaseIterable { case mmyy case mmddyy diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index db09b3ca..f0a613ad 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -9,7 +9,7 @@ import Foundation import UIKit extension InputField { - protocol FieldHandler: UITextFieldDelegate { + protocol FieldTypeHandler: UITextFieldDelegate { var keyboardType: UIKeyboardType { get } func configure(for inputField: InputField) func appendRules(for inputField: InputField) @@ -18,7 +18,7 @@ extension InputField { public enum FieldType: String, CaseIterable { case text, number, inlineAction, password, creditCard, telephone, date, securityCode - func handler() -> FieldHandler { + func handler() -> FieldTypeHandler { switch self { case .text: return TextHandler.shared @@ -43,150 +43,5 @@ extension InputField { handler().keyboardType } - internal func appendRules(for textField: InputField) { - handler().appendRules(for: textField) - } } } - -extension InputField { - class TextHandler: NSObject, FieldHandler { - static let shared = TextHandler() - - var keyboardType: UIKeyboardType { .default } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) {} - } - - class NumberHandler: NSObject, FieldHandler { - static let shared = NumberHandler() - - var keyboardType: UIKeyboardType { .numberPad } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) {} - } - - class InlineActionHandler: NSObject, FieldHandler { - static let shared = InlineActionHandler() - - var keyboardType: UIKeyboardType { .default } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) {} - } - - class PasswordHandler: NSObject, FieldHandler { - static let shared = PasswordHandler() - - var keyboardType: UIKeyboardType { .default } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) {} - } - - class CreditCardHandler: NSObject, FieldHandler { - static let shared = CreditCardHandler() - - var keyboardType: UIKeyboardType { .numberPad } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) { - if let text = inputField.textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = inputField.creditCardType.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid credit card." - } - inputField.rules.append(.init(rule)) - } - } - } - - class TelephoneHandler: NSObject, FieldHandler { - static let shared = TelephoneHandler() - - var keyboardType: UIKeyboardType { .phonePad } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) { - if let text = inputField.textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = "XXX-XXX-XXXX".count - $0.compareType = .equals - $0.errorMessage = "Enter a valid telephone." - } - inputField.rules.append(.init(rule)) - } - } - } - - class DateHandler: NSObject, FieldHandler { - static let shared = DateHandler() - - var keyboardType: UIKeyboardType { .numberPad } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) { - if let text = inputField.textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = inputField.dateFormat.maxLength - $0.compareType = .equals - $0.errorMessage = "Enter a valid date." - } - inputField.rules.append(.init(rule)) - } - } - } - - class SecurityCodeHandler: NSObject, FieldHandler { - static let shared = SecurityCodeHandler() - - var keyboardType: UIKeyboardType { .numberPad } - - private override init() { - super.init() - } - - func configure(for inputField: InputField) {} - - func appendRules(for inputField: InputField) {} - } - -} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift new file mode 100644 index 00000000..11845e70 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift @@ -0,0 +1,27 @@ +// +// InlineAction.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + class InlineActionHandler: NSObject, FieldTypeHandler { + static let shared = InlineActionHandler() + + var keyboardType: UIKeyboardType { .default } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift new file mode 100644 index 00000000..0f274681 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift @@ -0,0 +1,27 @@ +// +// Number.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + class NumberHandler: NSObject, FieldTypeHandler { + static let shared = NumberHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift index 5a81d59b..00f7a2c4 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit extension InputField { @@ -17,4 +18,18 @@ extension InputField { } } + class PasswordHandler: NSObject, FieldTypeHandler { + static let shared = PasswordHandler() + + var keyboardType: UIKeyboardType { .default } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift new file mode 100644 index 00000000..3ad802bd --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -0,0 +1,27 @@ +// +// SecurityCode.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + class SecurityCodeHandler: NSObject, FieldTypeHandler { + static let shared = SecurityCodeHandler() + + var keyboardType: UIKeyboardType { .numberPad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift index 3a13bc84..bcbc882c 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit extension InputField { @@ -40,4 +41,27 @@ extension InputField { return formattedNumber } + class TelephoneHandler: NSObject, FieldTypeHandler { + static let shared = TelephoneHandler() + + var keyboardType: UIKeyboardType { .phonePad } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXX-XXX-XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid telephone." + } + inputField.rules.append(.init(rule)) + } + } + } + } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Text.swift b/VDS/Components/TextFields/InputField/FieldTypes/Text.swift new file mode 100644 index 00000000..25715c9f --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Text.swift @@ -0,0 +1,25 @@ +// +// Text.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + class TextHandler: NSObject, FieldTypeHandler { + static let shared = TextHandler() + + var keyboardType: UIKeyboardType { .default } + + private override init() { + super.init() + } + + func configure(for inputField: InputField) {} + + func appendRules(for inputField: InputField) {} + } +} diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index ccb7820c..23b2bffa 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -349,7 +349,7 @@ open class InputField: EntryFieldBase { override func updateRules() { super.updateRules() - fieldType.appendRules(for: self) + fieldType.handler().appendRules(for: self) } /// Used to update any Accessibility properties. @@ -389,31 +389,6 @@ open class InputField: EntryFieldBase { } return super.resignFirstResponder() } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- - internal func cursorPosition(textField: UITextField, range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { - let start = range.location - let length = string.count - - let newCursorLocation = start + length - - // Adjust the cursor position to skip over formatting characters - var formattedCharacterCount = 0 - for (index, character) in formattedNumber.enumerated() { - if index >= newCursorLocation + formattedCharacterCount { - break - } - if !character.isNumber { - formattedCharacterCount += 1 - } - } - - let finalCursorLocation = min(newCursorLocation + formattedCharacterCount, formattedNumber.count) - return textField.position(from: textField.beginningOfDocument, offset: finalCursorLocation) - } - //-------------------------------------------------- // MARK: - Password //-------------------------------------------------- @@ -491,8 +466,7 @@ extension InputField: UITextFieldDelegate { textField.text = formattedNumber // Calculate the new cursor position - if let newPosition = cursorPosition(textField: textField, - range: range, + if let newPosition = textField.cursorPosition(range: range, replacementString: string, rawNumber: rawNumber, formattedNumber: formattedNumber) { @@ -551,8 +525,7 @@ extension InputField: UITextFieldDelegate { textField.text = formattedNumber // Calculate the new cursor position - if let newPosition = cursorPosition(textField: textField, - range: range, + if let newPosition = textField.cursorPosition(range: range, replacementString: string, rawNumber: rawNumber, formattedNumber: formattedNumber) { @@ -592,3 +565,26 @@ extension String { } } + +extension UITextField { + internal func cursorPosition(range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { + let start = range.location + let length = string.count + + let newCursorLocation = start + length + + // Adjust the cursor position to skip over formatting characters + var formattedCharacterCount = 0 + for (index, character) in formattedNumber.enumerated() { + if index >= newCursorLocation + formattedCharacterCount { + break + } + if !character.isNumber { + formattedCharacterCount += 1 + } + } + + let finalCursorLocation = min(newCursorLocation + formattedCharacterCount, formattedNumber.count) + return position(from: beginningOfDocument, offset: finalCursorLocation) + } +} From a656561073e5dceda24cde3bffda0bcb2dfb044a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 09:27:00 -0500 Subject: [PATCH 31/87] redfactored handlers to base Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 9 ++- .../InputField/FieldTypes/Date.swift | 9 ++- .../InputField/FieldTypes/FieldType.swift | 72 +++++++++++++++++-- .../InputField/FieldTypes/InlineAction.swift | 8 +-- .../InputField/FieldTypes/Number.swift | 10 +-- .../InputField/FieldTypes/Password.swift | 8 +-- .../InputField/FieldTypes/SecurityCode.swift | 9 ++- .../InputField/FieldTypes/Telephone.swift | 10 +-- .../InputField/FieldTypes/Text.swift | 8 +-- .../TextFields/InputField/InputField.swift | 1 + 10 files changed, 99 insertions(+), 45 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 10dd99c4..36eeb5f7 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -75,18 +75,17 @@ extension InputField { } } - class CreditCardHandler: NSObject, FieldTypeHandler { + class CreditCardHandler: BaseFieldType { static let shared = CreditCardHandler() - - var keyboardType: UIKeyboardType { .numberPad } private override init() { super.init() + self.keyboardType = .numberPad } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) { + override func appendRules(for inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = inputField.creditCardType.maxLength diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift index 44dba3ac..f8faf6dc 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -10,18 +10,17 @@ import UIKit extension InputField { - class DateHandler: NSObject, FieldTypeHandler { + class DateHandler: BaseFieldType { static let shared = DateHandler() - var keyboardType: UIKeyboardType { .numberPad } - private override init() { super.init() + self.keyboardType = .numberPad } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) { + override func appendRules(for inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = inputField.dateFormat.maxLength diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index f0a613ad..c936ca72 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -7,14 +7,81 @@ import Foundation import UIKit +import VDSTokens extension InputField { protocol FieldTypeHandler: UITextFieldDelegate { var keyboardType: UIKeyboardType { get } + var minWidth: CGFloat { get set } + var leftImageName: String? { get set } + var actionModel: InputField.TextLinkModel? { get set } + var toolTipModel: Tooltip.TooltipModel? { get set } + var isSecureTextEntry: Bool { get set } + var placeholderText: String? { get set } + func configure(for inputField: InputField) func appendRules(for inputField: InputField) } + class BaseFieldType: NSObject, FieldTypeHandler { + var keyboardType: UIKeyboardType + var minWidth: CGFloat = 40.0 + var leftImageName: String? + var actionModel: InputField.TextLinkModel? + var toolTipModel: Tooltip.TooltipModel? + var isSecureTextEntry = false + var placeholderText: String? + + internal override init() { + keyboardType = .default + super.init() + } + + func configure(for inputField: InputField) { + + //textField + inputField.textField.isSecureTextEntry = isSecureTextEntry + + //leftIcon + if let leftImageName { + inputField.leftImageView.image = BundleManager.shared.image(for: leftImageName)?.withTintColor(inputField.iconColorConfiguration.getColor(inputField)) + } + inputField.leftImageView.isHidden = leftImageName == nil + + //actionLink + inputField.actionTextLink.surface = inputField.surface + if let actionModel { + inputField.actionTextLink.text = actionModel.text + inputField.actionTextLink.onClick = actionModel.onClick + inputField.actionTextLink.isHidden = false + inputField.containerStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) + } else { + inputField.actionTextLink.isHidden = true + inputField.containerStackView.setCustomSpacing(0, after: inputField.statusIcon) + } + + //set the width constraints + if let width = inputField.width, width > minWidth { + inputField.widthConstraint?.constant = width + inputField.widthConstraint?.isActive = true + inputField.minWidthConstraint?.isActive = false + } else { + inputField.minWidthConstraint?.constant = minWidth + inputField.widthConstraint?.isActive = false + inputField.minWidthConstraint?.isActive = true + } + + //placeholder + inputField.textField.placeholder = placeholderText + + //tooltip + inputField.tooltipModel = toolTipModel + } + + func appendRules(for inputField: InputField) {} + + } + public enum FieldType: String, CaseIterable { case text, number, inlineAction, password, creditCard, telephone, date, securityCode @@ -38,10 +105,5 @@ extension InputField { return SecurityCodeHandler.shared } } - - internal var keyboardType: UIKeyboardType { - handler().keyboardType - } - } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift index 11845e70..8f4beac2 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift @@ -10,18 +10,16 @@ import UIKit extension InputField { - class InlineActionHandler: NSObject, FieldTypeHandler { + class InlineActionHandler: BaseFieldType { static let shared = InlineActionHandler() - var keyboardType: UIKeyboardType { .default } - private override init() { super.init() } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) {} + override func appendRules(for inputField: InputField) {} } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift index 0f274681..5961518e 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift @@ -10,18 +10,18 @@ import UIKit extension InputField { - class NumberHandler: NSObject, FieldTypeHandler { + class NumberHandler: BaseFieldType { static let shared = NumberHandler() - var keyboardType: UIKeyboardType { .numberPad } - private override init() { super.init() + self.keyboardType = .numberPad } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} + + override func appendRules(for inputField: InputField) {} - func appendRules(for inputField: InputField) {} } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift index 00f7a2c4..d1e7b112 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift @@ -18,18 +18,16 @@ extension InputField { } } - class PasswordHandler: NSObject, FieldTypeHandler { + class PasswordHandler: BaseFieldType { static let shared = PasswordHandler() - var keyboardType: UIKeyboardType { .default } - private override init() { super.init() } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) {} + override func appendRules(for inputField: InputField) {} } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift index 3ad802bd..b7f7ed2a 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -10,18 +10,17 @@ import UIKit extension InputField { - class SecurityCodeHandler: NSObject, FieldTypeHandler { + class SecurityCodeHandler: BaseFieldType { static let shared = SecurityCodeHandler() - var keyboardType: UIKeyboardType { .numberPad } - private override init() { super.init() + self.keyboardType = .numberPad } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) {} + override func appendRules(for inputField: InputField) {} } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift index bcbc882c..03dcc603 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -41,18 +41,17 @@ extension InputField { return formattedNumber } - class TelephoneHandler: NSObject, FieldTypeHandler { + class TelephoneHandler: BaseFieldType { static let shared = TelephoneHandler() - var keyboardType: UIKeyboardType { .phonePad } - private override init() { super.init() + self.keyboardType = .phonePad } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) { + override func appendRules(for inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = "XXX-XXX-XXXX".count @@ -65,3 +64,4 @@ extension InputField { } } + diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Text.swift b/VDS/Components/TextFields/InputField/FieldTypes/Text.swift index 25715c9f..34662952 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Text.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Text.swift @@ -9,17 +9,15 @@ import Foundation import UIKit extension InputField { - class TextHandler: NSObject, FieldTypeHandler { + class TextHandler: BaseFieldType { static let shared = TextHandler() - var keyboardType: UIKeyboardType { .default } - private override init() { super.init() } - func configure(for inputField: InputField) {} + override func configure(for inputField: InputField) {} - func appendRules(for inputField: InputField) {} + override func appendRules(for inputField: InputField) {} } } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 23b2bffa..8e3003f8 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -256,6 +256,7 @@ open class InputField: EntryFieldBase { } open func updateFieldType() { + fieldType.handler().configure(for: self) var minWidth: CGFloat = 40.0 var leftImageName: String? From 4ed2b3c8946746164fa7bb6762ba1b1cbad54a63 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 10:03:56 -0500 Subject: [PATCH 32/87] refactored the rest of the inputfield code into the fieldtype handlers Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 126 +++++++-- .../InputField/FieldTypes/Date.swift | 48 ++-- .../InputField/FieldTypes/FieldType.swift | 32 ++- .../InputField/FieldTypes/InlineAction.swift | 10 +- .../InputField/FieldTypes/Number.swift | 12 +- .../InputField/FieldTypes/Password.swift | 29 ++- .../InputField/FieldTypes/SecurityCode.swift | 16 +- .../InputField/FieldTypes/Telephone.swift | 105 +++++--- .../InputField/FieldTypes/Text.swift | 6 +- .../TextFields/InputField/InputField.swift | 242 +----------------- 10 files changed, 277 insertions(+), 349 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 36eeb5f7..1eebba46 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -75,51 +75,121 @@ extension InputField { } } - class CreditCardHandler: BaseFieldType { + class CreditCardHandler: FieldTypeHandler { static let shared = CreditCardHandler() - + + var creditCardType: CreditCardType = .generic + private override init() { super.init() self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 288.0 + leftImageName = creditCardType.imageName + + super.updateView(inputField) + } - override func appendRules(for inputField: InputField) { + override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { - $0.maxLength = inputField.creditCardType.maxLength + $0.maxLength = creditCardType.maxLength $0.compareType = .equals $0.errorMessage = "Enter a valid credit card." } inputField.rules.append(.init(rule)) } } - } - - internal func formatCreditCardNumber(_ number: String) -> String { - let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes - return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") - } - internal func updateCardTypeIcon(rawNumber: String) { - guard rawNumber.count >= 4, - let firstFourDigits = Int(String(rawNumber.prefix(4))), - let creditCardType = CreditCardType.from(iin: firstFourDigits) else { - leftImageView.image = BundleManager.shared.image(for: CreditCardType.generic.imageName) - creditCardType = .generic - return + override func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { + if let value { + textField.text = formatCreditCardNumber(value) + } } - self.creditCardType = creditCardType - } + + override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) { + if let value { + textField.text = maskCreditCardNumber(value) + } + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let allowedCharacters = CharacterSet.decimalDigits + if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { + return false + } - internal func maskCreditCardNumber(_ number: String) -> String { - // Mask the first 12 characters if the length is 16 - let rawNumber = number.filter { $0.isNumber } - guard rawNumber.count == creditCardType.maxLength else { return formatCreditCardNumber(number) } - let lastFourDigits = rawNumber.suffix(4) - let maskedSection = String(repeating: "•", count: 12) - let formattedMaskSection = String.format(maskedSection, indices: creditCardType.separatorIndices, with: " ") - return formattedMaskSection + " " + lastFourDigits + // Get the current text + let currentText = textField.text ?? "" + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + if rawNumber.count > creditCardType.maxLength { + return false + } + + // Format the number with spaces + let formattedNumber = formatCreditCardNumber(rawNumber) + + // Update the icon based on the first four digits + updateCardTypeIcon(inputField, rawNumber: rawNumber) + + // Check again + if rawNumber.count > creditCardType.maxLength { + return false + } + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = textField.cursorPosition(range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // if all passes, then set the number1 + value = rawNumber + + // Prevent the default behavior + return false + } + + /// Private + internal func formatCreditCardNumber(_ number: String) -> String { + let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes + return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") + } + + internal func updateCardTypeIcon(_ inputField: InputField, rawNumber: String) { + defer { inputField.setNeedsUpdate() } + + guard rawNumber.count >= 4, + let firstFourDigits = Int(String(rawNumber.prefix(4))), + let creditCardType = CreditCardType.from(iin: firstFourDigits) else { + creditCardType = .generic + return + } + + self.creditCardType = creditCardType + } + + internal func maskCreditCardNumber(_ number: String) -> String { + // Mask the first 12 characters if the length is 16 + let rawNumber = number.filter { $0.isNumber } + guard rawNumber.count == creditCardType.maxLength else { return formatCreditCardNumber(number) } + let lastFourDigits = rawNumber.suffix(4) + let maskedSection = String(repeating: "•", count: 12) + let formattedMaskSection = String.format(maskedSection, indices: creditCardType.separatorIndices, with: " ") + return formattedMaskSection + " " + lastFourDigits + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift index f8faf6dc..24f9fe59 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - class DateHandler: BaseFieldType { + class DateHandler: FieldTypeHandler { static let shared = DateHandler() private override init() { @@ -18,9 +18,14 @@ extension InputField { self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 114.0 + placeholderText = inputField.dateFormat.placeholderText + + super.updateView(inputField) + } - override func appendRules(for inputField: InputField) { + override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = inputField.dateFormat.maxLength @@ -30,6 +35,27 @@ extension InputField { inputField.rules.append(.init(rule)) } } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + guard let oldText = textField.text, + let textRange = Range(range, in: oldText), + string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil else { + return false + } + + let newText = oldText.replacingCharacters(in: textRange, with: string) + if newText.count > inputField.dateFormat.maxLength { + return false + } + + if newText.count <= inputField.dateFormat.maxLength { + textField.text = String.format(newText, indices: inputField.dateFormat.separatorIndices, with: "/") + return false + } else { + return true + } + } } public enum DateFormat: String, CaseIterable { @@ -69,21 +95,5 @@ extension InputField { } } } - - internal func formatDate(_ input: String) -> String { - let formattedInput = input.filter { $0.isNumber } // Remove any existing slashes - var formattedString = "" - var currentIndex = formattedInput.startIndex - - for index in 0.. Bool { + return true + } + } public enum FieldType: String, CaseIterable { diff --git a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift index 8f4beac2..7bacecc0 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift @@ -10,16 +10,18 @@ import UIKit extension InputField { - class InlineActionHandler: BaseFieldType { + class InlineActionHandler: FieldTypeHandler { static let shared = InlineActionHandler() private override init() { super.init() } - override func configure(for inputField: InputField) {} - - override func appendRules(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 102.0 + + super.updateView(inputField) + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift index 5961518e..9e653421 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - class NumberHandler: BaseFieldType { + class NumberHandler: FieldTypeHandler { static let shared = NumberHandler() private override init() { @@ -18,10 +18,12 @@ extension InputField { self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} - - override func appendRules(for inputField: InputField) {} - + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift index d1e7b112..892a8b9b 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift @@ -18,16 +18,37 @@ extension InputField { } } - class PasswordHandler: BaseFieldType { + class PasswordHandler: FieldTypeHandler { static let shared = PasswordHandler() + internal var passwordActionType: PasswordAction = .hide + private override init() { super.init() } - override func configure(for inputField: InputField) {} - - override func appendRules(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + let isHide = passwordActionType == .hide + let buttonText = isHide ? + inputField.hidePasswordButtonText.isEmpty ? "Hide" : inputField.hidePasswordButtonText : + inputField.showPasswordButtonText.isEmpty ? "Show" : inputField.showPasswordButtonText + + isSecureTextEntry = !isHide + let nextPasswordActionType = passwordActionType.toggle() + if let text = inputField.text, !text.isEmpty { + actionModel = .init(text: buttonText, + onClick: { [weak self] _ in + guard let self else { return } + self.passwordActionType = nextPasswordActionType + inputField.setNeedsUpdate() + }) + } else { + passwordActionType = .show + } + minWidth = 62.0 + + super.updateView(inputField) + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift index b7f7ed2a..f49800f4 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - class SecurityCodeHandler: BaseFieldType { + class SecurityCodeHandler: FieldTypeHandler { static let shared = SecurityCodeHandler() private override init() { @@ -18,9 +18,19 @@ extension InputField { self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 88.0 + isSecureTextEntry = true + + super.updateView(inputField) + } - override func appendRules(for inputField: InputField) {} + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= 4 + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift index 03dcc603..bfcd9ef7 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -10,38 +10,7 @@ import UIKit extension InputField { - internal func formatUSNumber(_ number: String) -> String { - // Format the number in the style XXX-XXX-XXXX - let areaCodeLength = 3 - let centralOfficeCodeLength = 3 - let lineNumberLength = 4 - - var formattedNumber = "" - - if number.count > 0 { - formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) - } - - if number.count > areaCodeLength { - let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) - let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) - let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { - let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) - let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) - let lineNumber = number[startIndex.. 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = "XXX-XXX-XXXX".count @@ -61,6 +34,70 @@ extension InputField { inputField.rules.append(.init(rule)) } } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet(charactersIn: "01233456789") + let characterSet = CharacterSet(charactersIn: string) + let currentText = textField.text ?? "" + if !allowedCharacters.isSuperset(of: characterSet) { return false } + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + // Format the number with dashes + let formattedNumber = formatUSNumber(rawNumber) + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = textField.cursorPosition(range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // Prevent the default behavior + return false + + } + + internal func formatUSNumber(_ number: String) -> String { + // Format the number in the style XXX-XXX-XXXX + let areaCodeLength = 3 + let centralOfficeCodeLength = 3 + let lineNumberLength = 4 + + var formattedNumber = "" + + if number.count > 0 { + formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) + } + + if number.count > areaCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) + let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) + let lineNumber = number[startIndex.. minWidth { - widthConstraint?.constant = width - widthConstraint?.isActive = true - minWidthConstraint?.isActive = false - } else { - minWidthConstraint?.constant = minWidth - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - } - - //placeholder - textField.placeholder = placeholderText - - //tooltip - tooltipModel = toolTipModel - - } override func updateRules() { super.updateRules() - fieldType.handler().appendRules(for: self) + fieldType.handler().appendRules(self) } /// Used to update any Accessibility properties. @@ -390,10 +297,14 @@ open class InputField: EntryFieldBase { } return super.resignFirstResponder() } + + //-------------------------------------------------- + // MARK: - Public FieldType Properties + //-------------------------------------------------- + //-------------------------------------------------- // MARK: - Password //-------------------------------------------------- - internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } @@ -402,149 +313,20 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } - //--------------------------------------------------- - // MARK: - Credit Card - //--------------------------------------------------- - internal var creditCardRawNumber: String = "" - internal var creditCardType: CreditCardType = .generic { didSet { setNeedsUpdate() } } - - //--------------------------------------------------- - // MARK: - Telephone - //--------------------------------------------------- - } extension InputField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { - if fieldType == .creditCard { - textField.text = formatCreditCardNumber(creditCardRawNumber) - } + fieldType.handler().textFieldDidBeginEditing(self, textField: textField) } public func textFieldDidEndEditing(_ textField: UITextField) { - if self.fieldType == .creditCard { - textField.text = maskCreditCardNumber(creditCardRawNumber) - } + fieldType.handler().textFieldDidEndEditing(self, textField: textField) validate() } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - -// case text, number, inlineAction, password, creditCard, tel, date, securityCode - - switch fieldType { - case .creditCard: - let allowedCharacters = CharacterSet.decimalDigits - if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { - return false - } - - // Get the current text - let currentText = textField.text ?? "" - - // Calculate the new text - let newText = (currentText as NSString).replacingCharacters(in: range, with: string) - - // Remove any existing formatting - let rawNumber = newText.filter { $0.isNumber } - - if rawNumber.count > creditCardType.maxLength { - return false - } - - // Format the number with spaces - let formattedNumber = formatCreditCardNumber(rawNumber) - - // Update the icon based on the first four digits - updateCardTypeIcon(rawNumber: rawNumber) - - // Check again - if rawNumber.count > creditCardType.maxLength { - return false - } - - // Set the formatted text - textField.text = formattedNumber - - // Calculate the new cursor position - if let newPosition = textField.cursorPosition(range: range, - replacementString: string, - rawNumber: rawNumber, - formattedNumber: formattedNumber) { - textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) - } - - // if all passes, then set the number1 - creditCardRawNumber = rawNumber - - // Prevent the default behavior - return false - - case .date: - // Allow only numbers and limit the length of text. - guard let oldText = textField.text, - let textRange = Range(range, in: oldText), - string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil else { - return false - } - - let newText = oldText.replacingCharacters(in: textRange, with: string) - if newText.count > dateFormat.maxLength { - return false - } - - if newText.count <= dateFormat.maxLength { - textField.text = formatDate(newText) - return false - } else { - return true - } - - case .number: - // Allow only numbers - let allowedCharacters = CharacterSet.decimalDigits - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) - - case .telephone: - // Allow only numbers and limit the length of text. - let allowedCharacters = CharacterSet(charactersIn: "01233456789") - let characterSet = CharacterSet(charactersIn: string) - let currentText = textField.text ?? "" - if !allowedCharacters.isSuperset(of: characterSet) { return false } - - // Calculate the new text - let newText = (currentText as NSString).replacingCharacters(in: range, with: string) - - // Remove any existing formatting - let rawNumber = newText.filter { $0.isNumber } - - // Format the number with dashes - let formattedNumber = formatUSNumber(rawNumber) - - // Set the formatted text - textField.text = formattedNumber - - // Calculate the new cursor position - if let newPosition = textField.cursorPosition(range: range, - replacementString: string, - rawNumber: rawNumber, - formattedNumber: formattedNumber) { - textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) - } - - // Prevent the default behavior - return false - - case .securityCode: - // Allow only numbers and limit the length of text. - let allowedCharacters = CharacterSet.decimalDigits - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= 4 - - default: - return true - } + return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string) } } From a71e3910509013a08b5a53c3e2d8eae1cab05cef Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 10:05:38 -0500 Subject: [PATCH 33/87] moved extension Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 23 ------------------- .../TextFields/InputField/TextField.swift | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 0479f817..bebeec13 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -348,26 +348,3 @@ extension String { } } - -extension UITextField { - internal func cursorPosition(range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { - let start = range.location - let length = string.count - - let newCursorLocation = start + length - - // Adjust the cursor position to skip over formatting characters - var formattedCharacterCount = 0 - for (index, character) in formattedNumber.enumerated() { - if index >= newCursorLocation + formattedCharacterCount { - break - } - if !character.isNumber { - formattedCharacterCount += 1 - } - } - - let finalCursorLocation = min(newCursorLocation + formattedCharacterCount, formattedNumber.count) - return position(from: beginningOfDocument, offset: finalCursorLocation) - } -} diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index 9a96829e..933b8f47 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -82,3 +82,26 @@ open class TextField: UITextField { return success } } + +extension UITextField { + public func cursorPosition(range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { + let start = range.location + let length = string.count + + let newCursorLocation = start + length + + // Adjust the cursor position to skip over formatting characters + var formattedCharacterCount = 0 + for (index, character) in formattedNumber.enumerated() { + if index >= newCursorLocation + formattedCharacterCount { + break + } + if !character.isNumber { + formattedCharacterCount += 1 + } + } + + let finalCursorLocation = min(newCursorLocation + formattedCharacterCount, formattedNumber.count) + return position(from: beginningOfDocument, offset: finalCursorLocation) + } +} From bef02f6b388a844a7244a498e39a9a6fed81d920 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 10:09:08 -0500 Subject: [PATCH 34/87] more code refactoring Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 6 +- .../InputField/FieldTypes/Date.swift | 76 +++++++++---------- .../InputField/FieldTypes/FieldType.swift | 52 +++++++------ .../InputField/FieldTypes/Text.swift | 2 + 4 files changed, 69 insertions(+), 67 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 1eebba46..5c6c9a30 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -9,6 +9,7 @@ import Foundation import UIKit extension InputField { + enum CreditCardType: CaseIterable { case generic case visa @@ -19,10 +20,6 @@ extension InputField { case jcb case chinaUnionPay - var image: UIImage { - return BundleManager.shared.image(for: imageName)! - } - var imageName: String { var imageName: String = "generic" switch self { @@ -192,4 +189,5 @@ extension InputField { return formattedMaskSection + " " + lastFourDigits } } + } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift index 24f9fe59..19656745 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -10,6 +10,44 @@ import UIKit extension InputField { + public enum DateFormat: String, CaseIterable { + case mmyy + case mmddyy + case mmddyyyy + + public var placeholderText: String { + switch self { + case .mmyy: "MM/YY" + case .mmddyy: "MM/DD/YY" + case .mmddyyyy: "MM/DD/YYYY" + } + } + + public var formatString: String { + switch self { + case .mmyy: "MM/yy" + case .mmddyy: "MM/dd/yy" + case .mmddyyyy: "MM/dd/yyyy" + } + } + + public var maxLength: Int { + switch self { + case .mmyy: 5 + case .mmddyy: 8 + case .mmddyyyy: 10 + } + } + + internal var separatorIndices: [Int] { + switch self { + case .mmyy: [2] + case .mmddyy: [2,4] + case .mmddyyyy: [2,4] + } + } + } + class DateHandler: FieldTypeHandler { static let shared = DateHandler() @@ -58,42 +96,4 @@ extension InputField { } } - public enum DateFormat: String, CaseIterable { - case mmyy - case mmddyy - case mmddyyyy - - public var placeholderText: String { - switch self { - case .mmyy: "MM/YY" - case .mmddyy: "MM/DD/YY" - case .mmddyyyy: "MM/DD/YYYY" - } - } - - public var formatString: String { - switch self { - case .mmyy: "MM/yy" - case .mmddyy: "MM/dd/yy" - case .mmddyyyy: "MM/dd/yyyy" - } - } - - public var maxLength: Int { - switch self { - case .mmyy: 5 - case .mmddyy: 8 - case .mmddyyyy: 10 - } - } - - internal var separatorIndices: [Int] { - switch self { - case .mmyy: [2] - case .mmddyy: [2,4] - case .mmddyyyy: [2,4] - } - } - } - } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index cbf85633..3fa3e652 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -10,6 +10,32 @@ import UIKit import VDSTokens extension InputField { + + public enum FieldType: String, CaseIterable { + case text, number, inlineAction, password, creditCard, telephone, date, securityCode + + func handler() -> FieldTypeHandler { + switch self { + case .text: + return TextHandler.shared + case .number: + return NumberHandler.shared + case .inlineAction: + return InlineActionHandler.shared + case .password: + return PasswordHandler.shared + case .creditCard: + return CreditCardHandler.shared + case .telephone: + return TelephoneHandler.shared + case .date: + return DateHandler.shared + case .securityCode: + return SecurityCodeHandler.shared + } + } + } + class FieldTypeHandler: NSObject { var keyboardType: UIKeyboardType var minWidth: CGFloat = 40.0 @@ -79,29 +105,5 @@ extension InputField { } } - - public enum FieldType: String, CaseIterable { - case text, number, inlineAction, password, creditCard, telephone, date, securityCode - - func handler() -> FieldTypeHandler { - switch self { - case .text: - return TextHandler.shared - case .number: - return NumberHandler.shared - case .inlineAction: - return InlineActionHandler.shared - case .password: - return PasswordHandler.shared - case .creditCard: - return CreditCardHandler.shared - case .telephone: - return TelephoneHandler.shared - case .date: - return DateHandler.shared - case .securityCode: - return SecurityCodeHandler.shared - } - } - } + } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Text.swift b/VDS/Components/TextFields/InputField/FieldTypes/Text.swift index 601560e7..38fd123c 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Text.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Text.swift @@ -9,6 +9,7 @@ import Foundation import UIKit extension InputField { + class TextHandler: FieldTypeHandler { static let shared = TextHandler() @@ -16,4 +17,5 @@ extension InputField { super.init() } } + } From 6bb6d31b39687be091d35f4b72b9c8b1933c73a1 Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 9 May 2024 20:52:46 +0530 Subject: [PATCH 35/87] Digital ACT-191 ONEAPP-7958 story: code refactored --- VDS.xcodeproj/project.pbxproj | 28 ++-- VDS/Components/Calendar/Calendar.swift | 12 +- ...wCell.swift => CalendarDateViewCell.swift} | 133 +++++++++++------- ...swift => CalendarFooterReusableView.swift} | 84 +++++------ ...swift => CalendarHeaderReusableView.swift} | 18 +-- .../Calendar/CalendarReusableView.swift | 32 ----- 6 files changed, 148 insertions(+), 159 deletions(-) rename VDS/Components/Calendar/{CalendarDateCollectionViewCell.swift => CalendarDateViewCell.swift} (68%) rename VDS/Components/Calendar/{CalendarFooterView.swift => CalendarFooterReusableView.swift} (80%) rename VDS/Components/Calendar/{CalendarHeaderView.swift => CalendarHeaderReusableView.swift} (95%) delete mode 100644 VDS/Components/Calendar/CalendarReusableView.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index d6c8c32c..f8b92433 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -10,7 +10,9 @@ 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; - 18408EDE2BE32C9900E8646B /* CalendarFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */; }; + 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; }; + 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */; }; + 1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */; }; 18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */; }; 1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; }; 186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; }; @@ -18,15 +20,12 @@ 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */; }; 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; - 18A3F1322BD944E800498E4A /* CalendarDateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; - 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; - 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */; }; 18FEA1B92BE1301700A56439 /* CalendarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; @@ -212,7 +211,9 @@ 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = ""; }; 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; - 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterView.swift; sourceTree = ""; }; + 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateViewCell.swift; sourceTree = ""; }; + 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderReusableView.swift; sourceTree = ""; }; + 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterReusableView.swift; sourceTree = ""; }; 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = ""; }; 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = ""; }; 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = ""; }; @@ -220,15 +221,12 @@ 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DropdownSelectChangeLog.txt; sourceTree = ""; }; 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; - 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateCollectionViewCell.swift; sourceTree = ""; }; 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; - 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarReusableView.swift; sourceTree = ""; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; - 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = ""; }; 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CalendarChangeLog.txt; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; @@ -445,11 +443,10 @@ isa = PBXGroup; children = ( 18A3F1292BD9298900498E4A /* Calendar.swift */, - 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */, - 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */, - 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, + 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */, + 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */, + 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */, 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, - 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */, 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */, 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */, ); @@ -1149,6 +1146,7 @@ EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */, + 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */, EA0D1C3F2A6AD5E200E5C127 /* Typography+ContentSizeCategory.swift in Sources */, EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */, EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */, @@ -1190,7 +1188,6 @@ 71ACE89C2BA0451200FB6ADC /* PaginationContainer.swift in Sources */, EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, - 18A3F1322BD944E800498E4A /* CalendarDateCollectionViewCell.swift in Sources */, EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */, 71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */, @@ -1206,7 +1203,6 @@ EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, EA2DC9B02BE175BA004F58C5 /* RequiredRule.swift in Sources */, - 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */, EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */, @@ -1243,7 +1239,6 @@ EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */, EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */, EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */, - 18408EDE2BE32C9900E8646B /* CalendarFooterView.swift in Sources */, EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */, EAA7456C2AB23E2000C1841F /* TooltipModel.swift in Sources */, EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, @@ -1258,7 +1253,6 @@ EA985C1D296CD13600F2FF2E /* BundleManager.swift in Sources */, EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */, EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, - 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */, @@ -1272,10 +1266,12 @@ EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */, EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */, EA985BF5296C60C000F2FF2E /* Icon.swift in Sources */, + 1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */, EA3361AA288B25E40071C351 /* Disabling.swift in Sources */, EA3361B6288B2A410071C351 /* Control.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, + 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */, EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */, 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */, EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index e2f101d4..fa843db0 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -95,7 +95,7 @@ open class CalendarBase: View { collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false collectionView.backgroundColor = .clear - collectionView.register(CalendarDateCollectionViewCell.self, forCellWithReuseIdentifier: CalendarDateCollectionViewCell.identifier) + collectionView.register(CalendarDateViewCell.self, forCellWithReuseIdentifier: CalendarDateViewCell.identifier) collectionView.register(CalendarHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CalendarHeaderReusableView.identifier) @@ -175,6 +175,12 @@ open class CalendarBase: View { /// Resets to default settings. open override func reset() { super.reset() + hideContainerBorder = false + hideCurrentDateIndicator = false + transparentBackground = false + activeDates = [] + inactiveDates = [] + indicators = [] } //-------------------------------------------------- @@ -216,7 +222,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateViewCell.identifier, for: indexPath) as? CalendarDateViewCell else { return UICollectionViewCell() } var indicatorCount = 0 if self.indicators.count > 0 { for x in (0...(self.indicators.count-1)) { @@ -273,7 +279,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { return UICollectionReusableView() } - footer.configure(with: surface, indicators: indicators) + footer.update(with: surface, indicators: indicators) return footer } } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift similarity index 68% rename from VDS/Components/Calendar/CalendarDateCollectionViewCell.swift rename to VDS/Components/Calendar/CalendarDateViewCell.swift index 40ba8b82..7bbe3fd0 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -1,19 +1,17 @@ // -// CalendarDateCollectionViewCell.swift +// CalendarDateViewCell.swift // VDS // // Created by Kanamarlapudi, Vasavi on 24/04/24. // -import Foundation import UIKit import VDSTokens -/// Calendar collection view cell for Date view -final class CalendarDateCollectionViewCell: UICollectionViewCell { +final class CalendarDateViewCell: UICollectionViewCell { - ///Identifier for the Calendar Date Cell - static let identifier: String = String(describing: CalendarDateCollectionViewCell.self) + ///Identifier for the Calendar Date Cell. + static let identifier: String = String(describing: CalendarDateViewCell.self) //-------------------------------------------------- // MARK: - Private Properties @@ -71,8 +69,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- - - /// Configuring the cell with default setup + /// Configuring the cell with default setup. private func setUp() { isAccessibilityElement = false contentView.addSubview(containerView) @@ -96,18 +93,18 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } - /// Updating UI based on selected date, modified indicators data along with surface - /// Enable/disable cell based on min date, max date, active dates, inactive dates + /// Updating UI based on selected date, modified indicators data along with surface. + /// Enable/disable cell based on min date, max date, active dates, inactive dates. func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, displayDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } numberLabel.surface = surface numberLabel.text = text - // enable/disable cells based on min date, max date and active/inactive dates + // enable/disable cells based on min date, max date and active/inactive dates. self.updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) - // handling inactive dates + // handling inactive dates. if inactiveDates.count > 0 { for x in (0...(inactiveDates.count-1)) { if (inactiveDates[x].monthInt == displayDate.monthInt) && (inactiveDates[x].yearInt == displayDate.yearInt) { @@ -118,7 +115,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { } } - // update text color, bg color, corner radius + // update text color, bg color, corner radius. if (numberLabel.text == self.getDay(with: selectedDate)) && (selectedDate.monthInt == displayDate.monthInt) && (selectedDate.yearInt == displayDate.yearInt) && numberLabel.isEnabled { numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor @@ -129,20 +126,20 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { layer.cornerRadius = 0 } - // add indicators + // add indicators. if indicatorCount > 0 { for x in (0...(indicators.count-1)) { - // irrespective of month and year, if it needs to show indicators on every month - comment below first if condition - if (indicators[x].date.monthInt == displayDate.monthInt) && (indicators[x].date.yearInt == displayDate.yearInt) { + // irrespective of month and year, if it needs to show indicators on every month - comment below first if condition, else uncomment it. + // if (indicators[x].date.monthInt == displayDate.monthInt) && (indicators[x].date.yearInt == displayDate.yearInt) { if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) addIndicator(with: color, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) } - } + // } } } - // update text style for current date + // update text style for current date. if (numberLabel.text == self.getDay(with: currentDate)) && (currentDate.monthInt == displayDate.monthInt) && (currentDate.yearInt == displayDate.yearInt) { numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { @@ -166,7 +163,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { } } - // handing active dates if exist, else enable numberLabel to display day + // handing active dates if exist, else enable numberLabel to display day. func handleActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { if activeDates.count > 0 && inactiveDates.count == 0 { showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) @@ -175,7 +172,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { } } - // enable all days if no active dates, handing active dates if exist + // enable all days if no active dates, handing active dates if exist. func enableAllDaysAndCheckActiveDates(with surface:Surface, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { if activeDates.count > 0 && inactiveDates.count == 0 { disableLabel(with: surface) @@ -184,49 +181,83 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { numberLabel.isEnabled = true } } + + func minDateValidation(with surface:Surface, minDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + // validate days to enable/disable with min date only. + if let day:Int = Int(numberLabel.text), day < minDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } + + func maxDateValidation(with surface:Surface, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + // validate days to enable/disable with max date only. + if let day:Int = Int(numberLabel.text), day > maxDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } + + func minAndMaxDateValidation(with surface:Surface, minDate: Date, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + // validate days to enable/disable with min and max date. + if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } - // enable/disable cells based on min date, max date and active/inactive dates + // enable/disable cells based on min date, max date and active/inactive dates. func updateLabel(with surface: Surface, displayDate: Date, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { - if (minDate.yearInt == displayDate.yearInt) || (maxDate.yearInt == displayDate.yearInt) { + + if (minDate.yearInt == displayDate.yearInt) && !(maxDate.yearInt == displayDate.yearInt) { + // min year and max year are different, and matched to min year. + if (minDate.monthInt == displayDate.monthInt) { + // min year and max year are different, and matched to min year and min month. + minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + // handing active dates - enable all days if no active dates. + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else if (maxDate.yearInt == displayDate.yearInt) && !((minDate.yearInt == displayDate.yearInt)) { + // min year and max year are different, and matched to max year. + if (maxDate.monthInt == displayDate.monthInt) { + // min year and max year are different, and matched to max year and max month. + maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + // handing active dates - enable all days if no active dates. + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else if (minDate.yearInt == displayDate.yearInt) && (maxDate.yearInt == displayDate.yearInt) { + // min year and max year same if (minDate.monthInt == displayDate.monthInt) && (maxDate.monthInt == displayDate.monthInt) { - // validate days to enable/disable - if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { - disableLabel(with: surface) - } else { - numberLabel.isEnabled = false - handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) - } + // min year and max year same, when choose dates in same month. + minAndMaxDateValidation(with: surface, minDate: minDate, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else if (minDate.monthInt == displayDate.monthInt) || (maxDate.monthInt == displayDate.monthInt) { + // min year and max year same, and choose dates in different months. if (minDate.monthInt == displayDate.monthInt) { - // validate days to enable/disable - if let day:Int = Int(numberLabel.text), day < minDate.dayInt { - disableLabel(with: surface) - } else { - numberLabel.isEnabled = false - handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) - } - + // min year and max year same, and matched to min month. + minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else if (maxDate.monthInt == displayDate.monthInt) { - // validate days to enable/disable - if let day:Int = Int(numberLabel.text), day > maxDate.dayInt { - disableLabel(with: surface) - } else { - numberLabel.isEnabled = false - handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) - } - + // min year and max year same, and matched to max month. + maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } - } else if ((minDate.monthInt < displayDate.monthInt) || (displayDate.monthInt < maxDate.monthInt)) { - // enable all days if no active dates - // handing active dates + } else { + // min year and max year same, and not matched to min or max month. + // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } else { - // enable all days if no active dates - // handing active dates + // min year and max year are different, and not matched to min or max year. + // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } - } func addIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool) { diff --git a/VDS/Components/Calendar/CalendarFooterView.swift b/VDS/Components/Calendar/CalendarFooterReusableView.swift similarity index 80% rename from VDS/Components/Calendar/CalendarFooterView.swift rename to VDS/Components/Calendar/CalendarFooterReusableView.swift index 1ebd73be..3e85a6ec 100644 --- a/VDS/Components/Calendar/CalendarFooterView.swift +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -1,40 +1,24 @@ // -// CalendarFooterView.swift +// CalendarFooterReusableView.swift // VDS // // Created by Kanamarlapudi, Vasavi on 29/04/24. // -import Foundation import UIKit import VDSTokens -import Combine /// Footer view to show indicators data. -open class CalendarFooterView: View { - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - required public init() { - super.init(frame: .zero) - } +class CalendarFooterReusableView: UICollectionReusableView { - public override init(frame: CGRect) { - super.init(frame: .zero) - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - } - - //-------------------------------------------------- - // MARK: - Public Properties - //-------------------------------------------------- - open var items: [CalendarBase.CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - + ///Identifier for the Calendar Footer Reusable View. + static let identifier: String = String(describing: CalendarFooterReusableView.self) + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + private var surface: Surface = .light + private var items: [CalendarBase.CalendarIndicatorModel] = [] internal var containerSize: CGSize { CGSize(width: 304, height: 40) } internal var containerView = View().with { @@ -60,50 +44,51 @@ open class CalendarFooterView: View { } //-------------------------------------------------- - // MARK: - Lifecycle + // MARK: - Initializers //-------------------------------------------------- - open override func initialSetup() { - super.initialSetup() + override init(frame: CGRect) { + super.init(frame: frame) + setUp() } - open override func setup() { - super.setup() + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + /// Configuring the cell with default setup. + private func setUp() { isAccessibilityElement = false addSubview(containerView) containerView - .pinTop(VDSLayout.space6X) +// .pinTop(VDSLayout.space6X) + .pinTopLessThanOrEqualTo(topAnchor, VDSLayout.space6X, .defaultLow) .pinBottom() - .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space3X, .defaultLow) + .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space3X, .defaultHigh) .pinTrailingLessThanOrEqualTo(trailingAnchor, VDSLayout.space3X, .defaultHigh) - .height(containerSize.height) - .width(containerSize.width) - - containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - + .width(containerSize.width - (2*VDSLayout.space3X)) + .heightLessThanEqualTo(containerSize.height) + // legend Collection View containerView.addSubview(legendCollectionView) - legendCollectionView.pinToSuperView() + legendCollectionView.pinTop().pinBottom().pinLeading().pinTrailing().pinCenterY().pinCenterX() } - open override func updateView() { - super.updateView() + /// Updating UI to show legend with titles. + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel]) { + self.items = indicators + self.surface = surface legendCollectionView.reloadData() - setNeedsLayout() - layoutIfNeeded() } - override open func layoutSubviews() { - super.layoutSubviews() - } - - open override func reset() { - super.reset() - } } -extension CalendarFooterView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +extension CalendarFooterReusableView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return items.count @@ -114,7 +99,7 @@ extension CalendarFooterView: UICollectionViewDelegate, UICollectionViewDataSour let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LegendCollectionViewCell.identifier, for: indexPath) as? LegendCollectionViewCell, indexPath.row <= items.count else { return UICollectionViewCell() } let text = items[indexPath.row].label - cell.updateTitle(text: text, color: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullcircle: indexPath.row == 1, drawSemiCircle: indexPath.row == 2) + cell.updateTitle(text: text, color: VDSColor.elementsSecondaryOnlight, surface: self.surface, clearFullcircle: indexPath.row == 1, drawSemiCircle: indexPath.row == 2) return cell } } @@ -133,6 +118,7 @@ private class LegendCollectionViewCell: UICollectionViewCell { $0.numberOfLines = 1 $0.textStyle = .bodySmall $0.isAccessibilityElement = false + $0.backgroundColor = .clear } private var legendIndicatorWrapper: View = View().with { diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderReusableView.swift similarity index 95% rename from VDS/Components/Calendar/CalendarHeaderView.swift rename to VDS/Components/Calendar/CalendarHeaderReusableView.swift index cdb08a18..b1d18810 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderReusableView.swift @@ -1,27 +1,26 @@ // -// CalendarHeaderView.swift +// CalendarHeaderReusableView.swift // VDS // // Created by Kanamarlapudi, Vasavi on 30/04/24. // -import Foundation import UIKit import VDSTokens -/// Header view to display month and year along with days of week +/// Header view to display month and year along with days of week. class CalendarHeaderReusableView: UICollectionReusableView { - ///Identifier for the Calendar Header Reusable View + ///Identifier for the Calendar Header Reusable View. static let identifier: String = String(describing: CalendarHeaderReusableView.self) //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - /// A callback when the next button clicked + /// A callback when the next button clicked. public var nextClicked: (() -> (Void))? - /// A callback when the previous button clicked + /// A callback when the previous button clicked. public var previousClicked: (() -> (Void))? //-------------------------------------------------- @@ -106,7 +105,10 @@ class CalendarHeaderReusableView: UICollectionReusableView { setUp() } - /// Configuring the cell with default setup + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + /// Configuring the cell with default setup. private func setUp() { isAccessibilityElement = false @@ -150,7 +152,7 @@ class CalendarHeaderReusableView: UICollectionReusableView { } /// Updating UI based on next/previous clicks along with surface. - /// Updating UI to enable/disable the next & previous buttons, updating header title + /// Updating UI to enable/disable the next & previous buttons, updating header title. func update(with surface: Surface, aDate: Date, nextEnabled: Bool, previousEnabled: Bool) { self.surface = surface headerTitle.surface = surface diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift deleted file mode 100644 index 4a695ab0..00000000 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// CalendarReusableView.swift -// VDS -// -// Created by Kanamarlapudi, Vasavi on 24/04/24. -// - -import UIKit -import VDSTokens - -/// Custom footer view -class CalendarFooterReusableView: UICollectionReusableView { - - ///Identifier for the Calendar Footer Reusable View - static let identifier: String = String(describing: CalendarFooterReusableView.self) - - private lazy var footerView = CalendarFooterView() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel]) { - footerView.items = indicators - footerView.surface = surface - addSubview(footerView) - } -} From 7965d4056f2ddd0691aa175e4408b3ea2c1f35ca Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 11:05:18 -0500 Subject: [PATCH 36/87] updated images / imageView Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 69 +- .../InputField/FieldTypes/FieldType.swift | 2 +- .../TextFields/InputField/InputField.swift | 54 +- .../credit-card.imageset/credit-card.svg | 1 - .../Contents.json | 2 +- .../DCI_Horizontal-2_onlight.svg | 819 ++++++++++++++++++ .../dinersClub-inverted.svg | 196 ----- .../dinersClub.imageset/Contents.json | 2 +- .../DCI_Horizontal-2_ondark.svg | 1 + .../dinersClub.imageset/dinersClub.svg | 196 ----- .../placeholder-inverted.svg | 7 - .../placeholder.imageset/Contents.json | 15 - .../placeholder.imageset/placeholder.svg | 18 - .../Contents.json | 2 +- .../UnionPay-logo-onDark.svg | 7 + .../Contents.json | 2 +- .../UnionPay-logo-onLight.svg | 6 + 17 files changed, 907 insertions(+), 492 deletions(-) delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/credit-card.svg create mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg create mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg rename VDS/SupportingFiles/Icons.xcassets/CreditCard/{credit-card.imageset => unionPay-inverted.imageset}/Contents.json (80%) create mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg rename VDS/SupportingFiles/Icons.xcassets/CreditCard/{placeholder-inverted.imageset => unionPay.imageset}/Contents.json (80%) create mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 5c6c9a30..42441574 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - enum CreditCardType: CaseIterable { + public enum CreditCardType: CaseIterable { case generic case visa case mastercard @@ -18,23 +18,25 @@ extension InputField { case discover case dinersClub case jcb - case chinaUnionPay + case unionPay - var imageName: String { - var imageName: String = "generic" - switch self { - case .visa: imageName = "visa" - case .mastercard: imageName = "mastercard" - case .amex: imageName = "amex" - case .discover: imageName = "discover" - case .dinersClub: imageName = "dinersClub" - case .jcb: imageName = "jcb" - default: imageName = "generic" + func imageName(surface: Surface) -> String { + func getImageName(_ surface: Surface, name: String) -> String { + return surface == .light ? name : "\(name)-inverted" + } + switch self { + case .visa: return getImageName(surface, name: "visa") + case .mastercard: return "mastercard" + case .amex: return "amex" + case .discover: return "discover" + case .dinersClub: return "dinersClub"//getImageName(surface, name: "dinersClub") + case .jcb: return "jcb" + case .unionPay: return getImageName(surface, name: "unionPay") + default: return getImageName(surface, name: "generic") } - return imageName } - internal var separatorIndices: [Int] { + var separatorIndices: [Int] { switch self { case .dinersClub: return [4, 10] @@ -43,7 +45,7 @@ extension InputField { } } - internal var maxLength: Int { + var maxLength: Int { switch self { case .dinersClub: return 14 default: return 16 @@ -65,7 +67,7 @@ extension InputField { case 3528...3589: return .jcb case 6200...6299, 6000...6010, 8100...8199: - return .chinaUnionPay + return .unionPay default: return nil } @@ -75,8 +77,6 @@ extension InputField { class CreditCardHandler: FieldTypeHandler { static let shared = CreditCardHandler() - var creditCardType: CreditCardType = .generic - private override init() { super.init() self.keyboardType = .numberPad @@ -84,7 +84,7 @@ extension InputField { override func updateView(_ inputField: InputField) { minWidth = 288.0 - leftImageName = creditCardType.imageName + leftImageName = inputField.cardType.imageName(surface: inputField.surface) super.updateView(inputField) } @@ -92,7 +92,7 @@ extension InputField { override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { - $0.maxLength = creditCardType.maxLength + $0.maxLength = inputField.cardType.maxLength $0.compareType = .equals $0.errorMessage = "Enter a valid credit card." } @@ -101,14 +101,15 @@ extension InputField { } override func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { - if let value { - textField.text = formatCreditCardNumber(value) - } + //reset the textField when you start editing + value = nil + inputField.cardType = .generic + textField.text = "" } override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) { if let value { - textField.text = maskCreditCardNumber(value) + textField.text = maskCreditCardNumber(inputField.cardType, number: value) } } @@ -127,18 +128,18 @@ extension InputField { // Remove any existing formatting let rawNumber = newText.filter { $0.isNumber } - if rawNumber.count > creditCardType.maxLength { + if rawNumber.count > inputField.cardType.maxLength { return false } // Format the number with spaces - let formattedNumber = formatCreditCardNumber(rawNumber) + let formattedNumber = formatCreditCardNumber(inputField.cardType, number: rawNumber) // Update the icon based on the first four digits updateCardTypeIcon(inputField, rawNumber: rawNumber) // Check again - if rawNumber.count > creditCardType.maxLength { + if rawNumber.count > inputField.cardType.maxLength { return false } @@ -161,9 +162,9 @@ extension InputField { } /// Private - internal func formatCreditCardNumber(_ number: String) -> String { + internal func formatCreditCardNumber(_ cardType: CreditCardType, number: String) -> String { let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes - return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") + return String.format(formattedInput, indices: cardType.separatorIndices, with: " ") } internal func updateCardTypeIcon(_ inputField: InputField, rawNumber: String) { @@ -172,20 +173,20 @@ extension InputField { guard rawNumber.count >= 4, let firstFourDigits = Int(String(rawNumber.prefix(4))), let creditCardType = CreditCardType.from(iin: firstFourDigits) else { - creditCardType = .generic + inputField.cardType = .generic return } - self.creditCardType = creditCardType + inputField.cardType = creditCardType } - internal func maskCreditCardNumber(_ number: String) -> String { + internal func maskCreditCardNumber(_ cardType: CreditCardType, number: String) -> String { // Mask the first 12 characters if the length is 16 let rawNumber = number.filter { $0.isNumber } - guard rawNumber.count == creditCardType.maxLength else { return formatCreditCardNumber(number) } + guard rawNumber.count == cardType.maxLength else { return formatCreditCardNumber(cardType, number: number) } let lastFourDigits = rawNumber.suffix(4) let maskedSection = String(repeating: "•", count: 12) - let formattedMaskSection = String.format(maskedSection, indices: creditCardType.separatorIndices, with: " ") + let formattedMaskSection = String.format(maskedSection, indices: cardType.separatorIndices, with: " ") return formattedMaskSection + " " + lastFourDigits } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index 3fa3e652..ed5f7e30 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -58,7 +58,7 @@ extension InputField { //leftIcon if let leftImageName { - inputField.leftImageView.image = BundleManager.shared.image(for: leftImageName)?.withTintColor(inputField.iconColorConfiguration.getColor(inputField)) + inputField.leftImageView.image = BundleManager.shared.image(for: leftImageName) } inputField.leftImageView.isHidden = leftImageName == nil diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index bebeec13..6307898f 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -45,6 +45,32 @@ open class InputField: EntryFieldBase { internal var minWidthConstraint: NSLayoutConstraint? + //-------------------------------------------------- + // MARK: - Public FieldType Properties + //-------------------------------------------------- + /// Representing the type of input. + open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - CreditCard/SecurityCode + //-------------------------------------------------- + open var cardType: CreditCardType = .generic { didSet { setNeedsUpdate() } } + //-------------------------------------------------- + // MARK: - Password + //-------------------------------------------------- + + /// This is the text that will be displayed when the password is unmasked. + open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } + + /// This is the text that will be displayed when the password is masked. + open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Date + //-------------------------------------------------- + /// Date Format used when using the FieldType 'Date'. + open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -66,10 +92,14 @@ open class InputField: EntryFieldBase { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable() - /// Representing the type of input. - open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } } - - open var leftImageView = UIImageView().with { $0.height(21); $0.width(32) } + open var leftImageView = UIImageView().with { + $0.height(21) + $0.width(32) + $0.isAccessibilityElement = false + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + } open var actionTextLink = TextLink().with { $0.contentEdgeInsets = .top(-2) } @@ -297,22 +327,6 @@ open class InputField: EntryFieldBase { } return super.resignFirstResponder() } - - //-------------------------------------------------- - // MARK: - Public FieldType Properties - //-------------------------------------------------- - - //-------------------------------------------------- - // MARK: - Password - //-------------------------------------------------- - open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } - open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } - - //-------------------------------------------------- - // MARK: - Date - //-------------------------------------------------- - open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } - } extension InputField: UITextFieldDelegate { diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/credit-card.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/credit-card.svg deleted file mode 100644 index a5975ebf..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/credit-card.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json index 01c460d2..6c8a6fee 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "dinersClub-inverted.svg", + "filename" : "DCI_Horizontal-2_onlight.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg new file mode 100644 index 00000000..569a32c0 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg @@ -0,0 +1,819 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +AClkpRPQoVUqp5YqpROIVtpgtzHAAIqX+KBcDSYUVthtoMNtWYU1fSwoyJRZztMGP5QGgkIAPVzT +gPcTYWHFodaALzILR1cY5nJJjAaFyToYy2ehVS5MxMkwOIQHFhQmzBMGWmZSWSw46HWbTz0UKY4N +0Aehb2Bh8rGOz0DbqFVKzKgwYZ4oZowWxiAS0G0YxGnJp5NjVmimRqlyMjSwsGJWGDISAYcixbGV +8aB4joipw3Foal0E4wWAMKopBSsMVigMnJhGAwIdoQYtzEJjDiwgAVeBRcGJoDSqcqAVnqlRxXQb +6sUQUIGgVvjRAaluRJMYMvcsu9fHITe0qqEIVBKOGsni4QsdjOMh4MCS2hujaaiICi4lBrWeIsWx +ZWiDHshCaNnuNPBZb0GCwZATAQqorBKxz54GPhs2mN0rwVLI1IYoZkAjDq2/EDWSJXb+MQBt9Iph +KBuxSxjOFNxnf8kPlOaz3SaRrbrc+CyQu8CMr+9iBiUGeghbVx6CwaCUIEAS9kA4SLwEEblAHohQ +gyMIgrryHg4FD+FhoSIIjSY9RLdVHAoegqDxSRB4hImkskfMNryDbilAKO0KpAAsLbPZQnI3BJdY +pUw5hD7Ew2FXCCwLIwtHg0DJ3WZwFiTE2wA7EZKOJoFqPIbMfWCAuiTDMHIaAb5vfuvHfWPtP8hi +XnvnuH6c7+8f14+3Fa4fc7vbX7W3dtePLf9b2D+vH//9c/24b4w9qO+qtzDfurnsd8bV41873la1 +49uzrZ3n36vOdctyv7Xv/LfmlWPsf924bvwzrhv73v2vGnfc+bfcV41v3xakNf/V/1s31pnrjL/1 +9u6PMa8/Y4591Thjfbu9deP6udz1rhtz/uvPmNeNsfW8brytcre2142tvVn3ujG3u/4btD+2v/4t +3L1ujO/9deNub+WY47Dn67PHtmqcse69YpxvvVhfbnnd1t/aMa8e21stznhb6d0rxlrz+nGuGXNd +P+ex95z7mnHH9XMZY78tKPPdQ+6rxv56fqvGfpW19RXja7Gu3/9fL97X++q3lccW1+59r5vzvlrc +ffb1Yq3zvr9iXDOu+2b/6wbpX3fHXleNN5nFv+qNca4a46yx1rXfj/PGtu66K7/c7r5uvn/l+etc +e+V3Z+5z5Xdb1f4//z3Xfr3+vte+ref9a97vBsq6Y2urv1v4d/6V137x5h33Xvv1e3O8ve+29nsv +/phv/3vl+PbOtef81o73313nyiv//+/KtxWuO/N9q797+1773Va88vtt17xqnnWu/HZ//66b89hW +frfw6mt97vpWfje/ddufceX3+7srv5vs2tx71R/b2u/W2sJvva/6dr8997hqj/P/v+brLdcd8207 +yOe8teYZlG3mvf+a88X6317z1xnbrC+v+ea/bee/d6/xrfpuq6u+2+p6z/XdVd/sd7768g7y94Zc +76qvttriXnXW+tqrdd3X68svr/tyG+vcvc6+7ot9t93fu+u+W1+N719pW/fFfmebf9U339otvvlz +7POu+ub9s/Wd5+6rvjx72c35b47rxqstbG2r/P/91d6Oed3byn6vu628a6517fzzaq+9Fd+s++ad +V3xrX22b7+2V13y5/5xXrjHvmP/LPQhjnLffGO98Nf79Vu6t5hvXe++um8v943qv37hrr7XHFd9f +tf791t33vv1/bX3WdXMbf4xrvluoc90153x/1VzW29qar+f3dssrvpus6mwrvq0NFNqbcbX3a8ux +//X//LPvuv4NBOIY17+F3OtdP2gLM//dV45/x5fv7nn92ypza7W29Vvsq//Y5o9r30D2cp+v7T7r +j/2ufgu597ZXn7mu/lp/cfXb6tfe9e2156sz5rr2DaQ3B9mLe84c5107aAuxzRj/yq3X1t+sscad +145r33/XvoH+xz5vof387tr55xzX3vmuXXPba9fb176F//J8L9ae++xBWmfftzB3nGvPuHrr9eW1 +65t59V5v3X/120r7rr3FPttqr64dhD/uvnLsNa722o6tBvm/tedW417tx1trC7/Vn/fKtxXuuHJ8 +vcW2cvw973VvINDeQmuz5nhfbr3Pvm7QFv7Mf+W349r79v3Xra++uXafdfd1b6Cfuc1bb+Hmu/at +L6+b2/1nzmvXPutf9++1651z5VuYf7/+Z54rz/tu7Wu3H19ducd668q3lf9eV47x9rXf/rfGu3K8 +vb0exLvevfYtxNpzW/f92edd+edaY+995RvIW86zl3G75253t7b2u7W2Ve+ad1s57zj7b7fu+/66 ++97Z28q3VeeX12+/tlpXrj/X2+p3fL+t3/LK7d/Wa135JtP32vyrx5UjQLbWBgo3GYazrW3V98a2 +Yr0/r3dbaWs7z/Xi3jmvOvuqvfbV4u+r/t1vIJ7r3sL8Me/V447xz/V3W6/duf6dQVuI9bW92qs9 +5tf/ardVv9jrjevFu36vd72/3m3V/+6b64o/93nXv3vHVW+r7ce5+m31va2d858r57ju++/fvupN +9vvOevOqPd78+lzxvQiQrdxA4eWZa423xzb3ivX1GPuKs9cVW1xz9jp7X3HWv+LLr635Y41rzndf +bXfHtmZ9O/YYY/9vtjVzu1/e+7U1a7x3/+Hvf/+Kr/0V5w2kcdbc8qy/rTnz/7Xu2W9ta/a+a7s7 +B4Ub/9y79RV/jm2322LLNe7+grzWGH/Pb8a84o5t9ncrtxWu2e/cr645e9x/xzXnbeW/3RVvq96t +rRn7XnG+v+b+L7YWw90apNxA/etv/cX9Z59BHvPb68ebDHN+Na/X1o/DvO/Ft+Zse/5W+9x9xX/f +mvPueeebva05Y/v/9zr7ikE825rzJsvY1lwzv5xv7qvN/vp8f78gzfXdeGtd9xZqv7/Wt/Zq8+5b +c3s/tr7azDH+19abt9X/Xf+O681X285117VzPuvcO9b5buzrzd9+rT23nH9vK9d1c9lm/H+uOOe8 +tbcV501WNeet1nfnXG/2Gf+L8cVV45wr3l133+vl/Na2f8s5xxnXa73dVev8sc9V423Vdf+7Wp59 +37xmfLG//++acd9/74zr/X9rzr3Puev7a+9aX6yz7RZ377nuFWNb8/bX6l8x3mT9Wltz9jn/XTNe +4X151rd/q3/VoM65ztbrurdQ65tx97h+rvvP2O9efe764/q5jfnm/Vu7va/334v7tRVvq40tx9rm +iv/1+ub6Ofb8681r5rn6bWUxt7Vv76+tvGec9ddVX921/7/mTaa7vZb77qvmds/af287x9zristt +1b21v/Kc9eccdHneX1d8t3Bbe33lXN78ft5xvdfajvXNtXMeY3t5rvfefffmH+t9va34esxr5zK3 +mvO9Lc5dV3wx1rn2vbX/lV/9v+cV663cQKCrrb+cV8ztnv/21nu8f+V3A2W+Ocbdaqw9rv1usuut +t5rXy218eeV3Qy1c3spt5b3ePfN9d965e+xzvdvyrTvvFfvN/99Wn1+Nd9YVe935/Xlv/H+9nM/f +3u77zd7vevu9+/9/+cVf//7z7p5rmzHPF9t67wayOOd9sd++3i3U/Pp7ed+dd+7v9tVebuOb9+c9 +X875r5jbmNufrf3V3py73l7/au8m69Zmv/uvmPP+dp453vpuWy9q4bbdatto4e4GABNnNwtnN1S3 +BaSnBWW5x0VxEw7TQUhPy4fqtoyFu1sJ4h6Xc2wqEGc5uznL7Y05jy/3+z3HAIggVplB3buBA0Al +BA6FQreVoQ12W7d13VbDvS3utg9XSQR7OG/w9MLQgObkr9vw/VIca7/6SSKf94m+8LNJoAqOI91m +I1+kgCLWTofDoJJfNRKJRFKPSLdd+3hUSbd1m/dwPL4v9hdZ7AULNQYSikmVEa9hgeJ4iI2GIsVJ +USjDBr2MQhtqyXTBkeqwud0WwSTEexcCGFkSwJC5MXOA3DhgG4vIZNJAzA0FTCQ4+fQdmJOhLEIv +A4s8Sii2hAlEgcbcFRRloQ14tMRSIafUQe8j2KD3TWabe2IcFK/bJAQb9CTVwSOADGwoHhvOIrd+ +Nuh1wM/SRhVw1oWR+0mVNnI/jJCzwe7jKFSHza20v8gVzgY9WzJdMEOE4032Q++KdhtDlIB9MZMp +CppaqASzZZShYE+DkKEREAEwIAGzEgAgGDQaD0pG0wk1vQMUgAJBOihKTlAyNB6TiUPCSCgMB8RY +jMIojqIojMSYYgoyGwSkR/Kee3NPFKU5hZEjM/pEUy6JdOamZYAn2yq3jHGxdNF+9w== + + + xtAonvICATAmrM83dZ2gR1KnD4mZx4Kpk2k33mPI8fPxbbDfVsc6wcpDWp2XDHWhbpC23gSj/jdj +6av319WK+TeLXWfNVgaRCdMQLgYYyZBjpSA21np8lVUswdpMNsoIOtqldOKk3HoN/leH5yf957+g +hyPz9cFLGNWpxbHx/PwEp+LKSKh4pzYUcp4XCJ1mIsMFDXlLVyNUIpssawiI25G7pGOLRGS9QQla +Fw4xqlVBfMnJfXLWQasbysLGiNQbqzWaTmgjmUBXS5h4hLGuO33zV+x6/vOA9Mb79uUGQfpigucQ +Pw0ykwj/Z5CriDURf4OEfplLCV6AE5g1BaiHVMhTs/4JIGmdAfCMHEF8g9d7aspmAWhHjpvpwfag +FaaPyWN7XnJnpLqTVqjptan7DbCGP1tieizHLoW7Ve4CQFYo53niXArH83c6UBY6VFvqhlxTNdCk +YcMMbqvxiHfZQvw2VINGN3p4UyMcJbE/QTLR/e8pe4aD3sjb2+0tWFiERMOnJ8n+KufuG8Y8UZ39 +Rr9Bx3a5h9ppSE6mlLiAKXN5R8BlTpZDZ86cLOgjV3c6hoRRpRymktssocHEgd50jh2Tf9sVWHjf +KtaBjdWh82/q6EooAwLRCSUN2yhHIYV5ydn5AdH9wAyoBISF2WXgB0rSGHwxM1XmeABkBKg9H5IT +haYPmO/AzbiXMuTFgqRsIC6DguK0YFShRLNp2S+hHSzh4F3bgO8ZrDfxBScmSla69Lpn+P5xqGbL +0V7teEc9oIuawvE6ie3pFIBITt4nqf2kv6vuMTl10xk+IkUReDu6hw+ImMslhNLPCGaY4jaGP8si +FQo/jfMzlg+TfegZND8cCh2HVuwTWCfTCwbyh3ThByhoVGuRvP+tzdaKVFMKFU0qEkGgIz7T2Qsn +b5oB4gTOhne0s5InuCsYbyktCKAmxfWhc/nRvQY+JDiFQkXxE/XN/i7ETZG6suvRDsgL1fKcMZzD +wJICYCy9wVWh7MWAvzFFp3kFOePPh9JgIxfntccXVA5aBSGsAt4O4EfPFdFqoJ8sxrwXFfcZNTqd +j7ru/pXPZyc4fqOw19LgPjpLTe4LV2lAJbnzQFqVrPkV0tOXfiZrF4dpco2MChDu93VpVCyds+Ko +PaElmLakWCzRqlcswzaWx24eUEwjESgE6Wqk0AP91heJ9BILZc3+mrLCIJ0ChmQLbvTNq730YtHZ +/fk9igZ9UZ1+vxmAtpSEwumOtuOcnhn8KFkNtKxC0AzndO6hCWSHB70VGtEcNLdYZFTXCAq3E7L3 +7dwzyg/hijVo4K4wXRYqOFj2zExkl6Hsw8RxkGvNXXK/WTz0KjTZpwZJybOc+AVosy4H8pTUWXBS +6y0rK9RYvrMApeCh/pkeCrX1rTtnmt48JioO+QFf3vJSw+wB4WOocKNbVt5cf+YAZ2pA+W+Y0weR +rBQ8RCloC4RwWS46itnWMnaSBQFUL/wJ74G4rnzKf+unWtowPqw+sYWPi3z1TM/xvz/2gvPgafaP +xSHK78QGQOz9VpWOhmNZVg5/RgVog0uy17Oi/Hu5IRl5cBEoou9bsCMOIXTTSrlPMpFlwUv60gOA +QzIi1eHMLfNolAkISGYrWmapmcxHvoxJq9snXx3dRBHmWHZqbTGxDLLAjuHncO+Vx8ORKvrlAKXz +7XwoOxy0incbCGxISyxD3CC3KPNoH+vKEbXZcKxuHylbwEEAv8Nrd79vBbR5OJVaeMHX+6u5mAT4 +K462GXgAHh27nTQIlmMLIEuB4ajhZdu1qq1BhCBYwB8vd6XWaI3AvhA8H2KvRgdMmSCvTSkGkcDF +yaflgEdC+hHyS2EhnafgV2swJPj5c6fxqg2kkg1F6d5sK9humTPI13S67ZgpcFVcUKmcNMzZN2aB +AyoLIh5eMocZXdXlF5avG+WYYb3iyVfzqmKOMAtjrv+u0jv/PrktOJaqeute1fi3ml3UBZeQcMoj +3GbEQc3JHl8UQgg73Nvkgfty0+JMAwRmnqbPx47lfn8ORu4xezG52xEp2pMSc2DcY3kAtV4L0ERB +gLCkMBkTHheNmBz8aO1bspjLtdGc9a82xKUlKEVTNMaI1DGnsD8Dd4ie046X00FXm+eCPNBPdY9W +keAlOvYo4TEFIWUaBhwzU6PfICiirMj+syUObXugQtdh5PlQ6+8lA2PiQ8i7hAOB+UnS4OSI7LCx +PFejodkJkfWukwDbRB8oDaQm5hp4k1O/nWa6rWBqYkzQ1LxARr8jw3K6G35WDjYfK0UUNSxohqot +ZRwmvWFXatJEB5+NUOhZ17DiTbnL+YB8GXolBGco4NvGiufye0T/56gfreXBaVPmVeoTj2FhvseN +bqdd2lvxCuoXAhkBZJdoNSj8EZYTINt6Y2+92IUoDSf+aA94yIJ/3gBGfIEa2D+/pj9Vi1Bm2whL +ImUQOiK17F2bBwiTR3pJ/+LUCVW0wXCy0LMTXi4giIiP6THLZXK8DBFgNGSg76WcNHiCLOWiND88 +QwrH/SKY0piZxtLrMpZOi/EZ6KhoRhSil/l8BHPYVqDCmEBThg3IhmjEpV63xNPHAlyhBZFP7IGQ +huRVkWUSLBFcGBgUcY6Gq3ICYrLh7AMEvoTdJOW1fw14iIpZw6DPX1C+0gAEiNVNATEe5fFCgsH2 +G57XDqcazIQvVDxeMQZ2zO+2HQe8lqRf+c1CEPkAdQUceK++0CA96Wihpcp8sYF10Uky28Wh3A2K +zwMTkDln/srDWgJHLmwcWBhFqn7GJhmYdxfXBwewURySJAk37IYd0NgzOdVjpfBtk5xhYWfCf/Se +evGgqp5cadB0LZUwrisHiwYXVIqlKge6By36p+U6Jfv5QJ1o6LzzRDY00mLkeKLQCxyyCDJcYurj +DWPmysr0BD+KXQrNX0kDfGCEMNDKwjhAeIRMnAGoamgDJYmRThHBgPkFxBuO1JAqeJguC0dEcYdF +gHBc20Dg5OD55Y+gVzP1yuypOd8+FJuvR8awv76cBfWJngZdcVCoDWIwfDTncq4+S1ctVHAVw3cx +l/DpNRHK5J170rEfuRvn+ltU8UXUWffcPDr4zpHLIPTprso9+P6AnEokkzgYNxeUlhveWikvmcGg +J5m+NHA53LCn9YsszYbbiRfITsse9Jit50Q7jl4UIcX2DvRTLE1GZUcZcsqvyI3G180uDh/N1g/U +pkawLZGt3E6HVUZ46qsw0E4FjUx5Hs0OocMWgtGcoEjQs1jtpxXy2uV9iV+v+KDG8cP4mqEdHltf +Qk9j6erEaTfCXERTG4KYKdiv4I954kmwytlB1P7nuLvisY5CC0tbn+F8zQVCJmQaAO4dlEaDgJVW +UFKAXy3ZTcQdFdxOIrR0VvINaewhj1BSrgJJeR6mLyhncZQftg53iSX1smM/NHMVgaZiy9OpF1aQ +qBpPY+S9pRduy00cyqkXLiFa9P66gSqlvu5utkllH0srdoTLvwmflYTKilbnW+LKfDprbf1a3w3n +nH/bRq+TvnS9yVPBPd5jXv85xvNDJuTy7KKQoXGSAvsQQtJEQinCvnds/A/CWM5Xyt9lwVhaLPdZ +AOspVQCYzo7h2a5Xm/zxtk0aSE/IXk3/MAyVJhm0IDv+vf84opceKZjtcvTczknoNnNju8cBfZ0H +6ccdQJ7F9KPNHFti+kFbDJ/wxPaTmelnMFPRsMYS4JVymmIpOo2/Un2mUFMsZT2zzIGLpfQrLQfF +0vbm7Cs1fyzZGigTwQxXZtSJm2Pp5dUw7mq/KA4lloImxjRfSVgpllQxxb7ScyKW3r1t65Wm9rG0 +VFCWvlJCRCypTTvnfaWpF0tT6D132eU3OEYCm9emxORJUSTRDzzRcJ2JhTRHQ8yLeV1YdhLk7wqr +Cy9M/Pmg61A2yuOElMohTe8OoUl8bndiDbuBSI58/Wj+g54/pu6csYi7yak8CdYMnGcuUyVIT2SA +dYZQXzkr1w95UJtgFtiwdViYRbFhXqrGDHNvzojvkHmGSIxeumfIhBZIKOX02BTHfnqC7EDCjzct +gGOpbRqWA/+BaCEsm/jlFVbTW6CIgZ9004Ij5aw0ISMWWKYCI9iKjenGmmFhHctQD0u7i5ByJsEg +sWT6g6zvEocv4L/JbPYXqImxHrSBgFjuxbDFO4UB6WZXICPuGS7AAynbJWPljptRKz9TXpCFoSQd +/YCmVUX8wNihMGbBZ3WBIPvynwsmR9rSp+zRismEKDIRrtpz9rlDlBr2z9bXvep/0iMDD4Sgsr9k +RIZ8XD/Bkt+bGbgFBWUu/xwCZ4lV+rnbVifYXO71uR/RUkEpiBlUrQydJELJ/1n0xbAkyl03dsOI +h3iPaUyjARB9BEr0Yzr3a8UbNv+PhlKlsJp2Bltpulkp3kKbSQUtEfXYdNIvHGGToKORzeBYibhh +h0Qz0bbps6gOyWntGKnxdlErCg/Rjl3M0+1fYt00Ujy8V2xiP/UtwzdvOm5305E4Rff+PyYh8hO4 +Cz9ywT3+3OBTwyGOPjo5ZshBe92tjzErSSYcAhO/Th4sYrDjiVHuvT8wwdDm2fK9iWyQlD8Ty4kI +FS25iZGIOrFK70NEmqUTzlNv+BDO9tGGMZHVtPdG0aqStwjhFmZuUI8EX38QeW1UrGIiuxdRKghj +XsVCORh3eUEMkwQeYtZd1Lu0ITb8twv0IoWb1XhMYoahAoFcxPS0psmOi0oI0Xk2/FHFeSxSsQB8 +hiV17zNFPqIRF6gNviQrG1m92bDsGYJan85sylOWF8SbbLpxmTmFnMbh7tZTBt3yZzRWRNxF6CxR +5laABUUWs9H6BgZn5f9h5sScLmMOjsMyBMQVMzm826lvrUZZy4BLFUOwQBfF0FpvvYOvbYelkUWo +LLMjTdcJTUklVjIVWUZsisgivhnAfdGjOVIvHUOBojdNnsuJcLWeOGvLi83QZocv4G7vhcfYMf9C +xr3fEJoocdYHJJ0kNZ6vSShFtulLgls3dFZTXleuL1xbMB6F0QKKbMWSLip9naacI4tpJH6bvM9N +Pp8pf1z/P8EFerEC7N/iXgpcqnIBFohU8/qSXssU6zKvotPX0XHVuoroHDqZZQDUGp1VZB6R9NRL +nydMTFHBrNSrcy7UBG31Oztdf1bo9HX9ZPR1LUT0XWdbdF4TJ/quPk4zLaf1X1gg/2I5MZpxcqly +8n5SU5lxsricAGaO3zjpvoUnRKaFDSFavEDQshMts5HxHpoe0UqDiSdaAUfQ2k20tizfdAlafric +wr/0rVoEIpGzQ1aLaFtNa4iQtpzBtg68z7eUNdoW3tKWilSgbMszce6lrWNVc9L9BtqW1XvUJqSt +TrCtS81EaEsS2A+PYJwJh0U5UineIb0hAiXxuTzS4CbIpUCPSepiNPc4TvfMm6gCMYFEjACgtBvk +ABdnOCNEqSLSJdjK80fzIkSnU5TIEVZlj5E1UF5vLfjwgzg8bVD7IVm56icLbtIe9A== + + + aahK2aDX4VUpxEEpaVlGuQnT9/2Z26Le/lA6KanCi6gGiW35AdVeqgLlt7NgQECDmYCpXle9YZwJ +Aolqd0Mg9tGR+BEQOVi8phjzY2hCMHRCAAtoWvtalU/Akr1ELF7awirWLSRnJMtiya4d7tXViLQE +q2fUseQsA78H2GdH1+DKCRaM18XdKOiSFUXfAQqvtGGwDAA1d/6BRVy+h8SCGxP8EzhlB31BZEZk +cx/61nbEWBPIi0F3M/PEiNVW7bA+5VHDezepm81hK0d0IxMtJMO4J0qj7E8IszcY4ggHcUcn27fs +KH8wKR2DhoRZUxJ1gDiESAyh/K1FnOQ5/jdQ6wUC7wQGCjleCFTp2YI7zhQhJo42DcayAWwIQslS +9NBhO5TFm6wMeoH6LMQBZWTMsmu08iBcdZGCIdx9cKWo0TpshCnNs8uA9q8fl4tloEosCA+Bl22k +XhZvXAZYlGx+86bOX2kpYIEG6HQ5lJQSVRYZQo7Tq7fEJBI32z7JkbFxOBAVDIxrljN2DoU6R4Go +NHWd2uNSpKPqdLQcOsC6aBQSOGOkwm3q7+P+NUYdeRdVbUPoKrWHN318QETcaqhJjJvg+HVEHh/l +Srj7Ks646BP6lB/ZR7oeoiglrH1R5wLYh2sibkx7FW8+9SEhsWf2OuvL1yBUlivoRM91lJArkRGj +l52wsT3Jx5T8oDFVYeVlqO6BbGIrppJ9pbYj/hQKkwDbr/Gyuxlv5oNkSbRVthlAMvP86NV+DQNF +Trc97dEbqIsOQYCzO/V9C+WEQvhGYcxAsAoCFhHG+m+LjnGmYX2NTUUSqg/E4guQU5NTr4ie5ACa +jcltD/NjjKKbXgUFpuetCRGZZJcv042Dj+/DLgRSpHmUrTHPa5HhmQpr5hj9GwLxiX8HMu+ckwrP +MT5leAYTZ391rcZQHb8CeVieVtwZSNB0z5p3TNfqbrsnP1y62lBkMaE3q0Ix8agUpw5bg8NgfkO2 +kLAVSVMcquw+cCkdrMYJqr2z7I443XJDpZk1EzhUSEgHTwkcDnhfBFc56DDRKLEKeaj0eADOSCaa +E1/HhxCphsAFLuCzRuLeFQ26smLVnBqjDMSRnHCE4uSP45JYMyaON8ET4yMTZmJ15e3oOltUXwYS +Y5PRc+sRmwkcjNTjRx1bTJkLfEfv/UQPAWjM2hYTy/C/Iq1N7ENk/GHErf+hsm9I5qdkm6MAc2mH +PU8cM6YlX9ZBoQCh+kLmwoRqrngt51pT7flHM4hN2TxdZ/dT+6QGVqPSxiOl/wnPykeP4J8k3skd +rCQOgmlwGPymVCn6noiIYb2Tyr7HoJaGw2navTFMK81UEup42QqXHR3YhHN/np96GzLXQ4aIykRG +5bfvbbX4FjCXsNRcKm6FwmJFOOaBmWIR0uW2eXsTc2X345qaeXotvjjVcbQYp2rR09T9CllgD8+q +mGpPuqU36bjQUvVV0QpMDJnb/5Ghf7gyyt0L9v+B0IeG80RqD3c6ZRBlkOErC12lDh0LTxykl39i +xp3pRyfiHanb5/WGHgsmZTbeV7WiNeL9TPeJ5vcst6/ieCsP6apI7VBPkZM68YS+S97PDJqshFmG +E4eT+IS/ExruFVkIFSdjhUcjCsCiBZEZuBfxLrH+ByQu9vY/ka8nVYe3Vyf/DCNDRZYP6ub2W6ch +ST9mbqvKx76GubYY4d1EFOCyJBSB4Z0c8RvR3t1EPvghE5uhHndvwx+rGhFn5I3hbTaV+J2nm34R +N3ry8DYM5lWxBvLqE73wtnkj4bgbggUYWW0pzn6honUkspG7M9FIjltSdJbjpBEF57zzmYugcZCF +4kml7+5qduHwgLezxYXrW3KUh0udRSZUYbmc6YDd9rr7V3jT+0iA7saN91S0uCL9pGakUoU3su4u +j0o0QkAbZhr5wXYa/x7SNuF/aM3dHeX4l0ZdHbubLQO0/RjfntaHxamHIY8SPaQSIBO9gkO0XZ9f +BF1zXa3el5rbDoAIvXzr5smKebuRb3fMTTinbcqFdJzapfIqwoiv7XIBZub2YDQEvV2v22rwBcXt +aNT2vTUYKAvcK6/vLUK+GOv0jgu9FKacKwDQsS88EFHbtC63V+gjCmujb/CfCpqK27GZzP7hubR7 +XnWrccXkhCZ7uohm+zq2ovnhTMipvnZ+PF4NSSvqmzD5SO8hknHwUOnWCEQcWxQfzV0bRCNVIqNQ +fZS8D/iNTPmo1p2cOxBH3kMdPSIxFo4l5j0Mol3xrBe2ONCqRsSYo+PHdtCKgOtZ7Irm2tJSLTLL +sbMYW/K1RT2Rlth6RP1MkjOY2dVvnpQZaHcIllU5qOGIY+utQZPf4w/49XDmQb3Z9fpjVSOwi1x5 +9VAthuc99HMfBDtRYTQk5eg/m/oa+cpRCDtCQrCqaQkjD584mKyU8+iNRCPjfK6NL+JgjkI7dk86 +IoK9ojhE6EitFwJOsisNAejLIHSZrQ6zZKM9W5kswi7mGI7de2hDsvS2Q8Y8a7d8/Ksm9EFFER1e +s5hVGszUhurwQas++lldoRH6EwuyrREmtoGFUDbhtizKbWbqGiJ0dSSBWrk1pJGc7ui/rRZ1xCB+ +j/RD1yWk9xn/QgIpI31kVTOLm1cnnoqyAzF20mdwyHT291+8BoJDdGIQu/0jvg39SpZFM9Ww6L8R +9sd/PaG2G8YUP8KJFocGyKWDaVkN6+rURZpjb17Lo/zGDFgXRVH7f+0m6YkyjX1R/M1sg2NwVFqs +8n+GZAUXBGd4G1PfNpeQknO+3SXcY8jG/1K8oGPbegbY+0Yi/3WtNNfMRgmjrq8gun0q4RLscHIc +xap/wyMa3CWNKzj40Zma17cb5Jmez9Rv1A6PNfOyx60KLyoV7KVvHoc2Ds5YKu1VuwlQKI/OU7HG +v7a+hUXNdNTgyGTrqCKl3ao2B1wF/mOYhLFRmQTM37sA+OrCAlys2RROwljgc004Zjx3diHYeR0x +WXdN/m6etcGbUuaVxR7nxqeLAK9Jdm5XrRxATT/B6UWtQePkXzvw4QOAHVNK4b8SccbNxtGZAMAf +d4Mi/fzyR2ph9HYubA3YQJ0IEvqSgyeyxz2g7vUdlQEc6L0PayfSNmcs15gGXTts2w31Q4UTosW4 +717M1YqTP6mTdmX259K0wBmG/KDtHwDQgmDWF4vYwdbFc/pCff1cXhpiDfiKqJtuwALtObvBph21 +rJuKuGFjTcfQ4sMj5BoTXhefBSrlmmwzeJoES1bnWUuA/nz5rHe/1cXQs76WgpCH1SiE1OZop9V4 +AmxVAS3ZAWtIlBfguKzZ68kY9pljAlwsUQloPzABrBlBRXvIDqEgCQ3t8w+/c3P4M7ASUm6D1KbB +WyHMjyjG+SEyAMWyk9kmdnONFU1nO8Om0dh7PBsGPP5uBSG9IwbQsYcxabhuHm9g0hkMLejDhMlc +jurkIVHXHDoodeh2B3bhAVKIroM5ugPPheY/Mq02DAKv1XOgDKRta0hPAJ8t0k+sz6K2tcTsVMU7 +IUqHeeCnHebXvzq4THjeHRRU4j/L0jTaRFzmhCvXa7jIbVj3V7qccDYRewESv1y2QO7iSw3Yk4av +lQCyDiQi2XkSRUXvF30t/MW3NZChcvlYL13CWyg6zU2UNNJzwLPmcnP370idxAHuIGnvFu6JK2OH +B3tj7aZWta4MY8u0nk78EZ+Jf+I6zQ5QFeeGT7v+yPGHYRv+fxoaoaP2FeIFEVYll4uY0V6ovLJe +zTe3hxoB2UQux353vPe+NPaMdOXpStOvxaW4Q94cjJNxcLe+EOZpeTQ2Fi2YzrEUpabd2zm0SFMC +kaemNMbllCPgJxkg0k1OHaT6cs3dUoct/GDXoAlgmY07B2kWFGy8rqVXxXgBGV8jtTY0nIXmqf8P +urO//XCtHGW6KqGL303hMqvnsxS04gCJuaI4hWbDx0X0KnCsih71SbobWb4ilTdGd1kWhLEz5v07 +DBRTTtkWJw1wMbnKHzoMi0b+Nqiy5Aw3sTH/u66Bdr7eDaqGOBpHgsm6qTVtFpyhaZki7Er94en9 +HoTie08xxFp5V8NkW77GiiMQ+VQmMiJinBitkNJ7QDyVQ3TMt4i4MY2AiCd7NdHa0QCxPMVTSEMS +RG3ViIhvCDUIAoyM0jGQIuKpVytYaNSUAPEEtzYzdJecUjyFtyXv7j4vFiwcsH4gqiKKp46CsL68 +qicuHOJJRrqqfg8hap+3mHYQOwhHK5IYcbjo4yUzCxbHz6KlQkaSM8iRueXQnO0zcARblt1/r/2e +0XNBEpIrk3ZXRHAAzmMftXYhE3n/i01NQN2/6SQfFHfa5F2+tb1B70Mo8RAvGLHMKQMQp0HFVfB/ +eRJ0AjdAqjEF3l4NuDoqkvTt4/z46VKN5j37u5fHIpA9NUULXJ/1CfbRSl16o4UA4451CNLPRGtN +q6+VcqNB4Zzm6/s+pWlWCaKx4V+EgBt65vGy7WgcuaHFHl2Mi3glouQzKYnF2IHq3RfugORg1f5Y +podmzzpgiTtpCXVYqPfy1JZyTNRjYMRJ9he2y/LSrr2FF8G9YaIR7OgB4L4kkRjcaaX4ye2JbVNI +gxpw1WrMGEnkaiCuTp6i2MYyrDlMq5g6kZjp5TiHB8gGiF9SFFNhkIk6TBzEEajxFNkeTxVXbJxv +zWDjycsp9le3DGo85WVyROGPp77Heu7js/Tx5JnOWnk2H8s9d+Op/AXSb/G4fTGqoO3wFFDfHk+h ++j9zaTwJ3Lg0mGiiCW08ZdTYdair6B9PzYQWIxTmcBUiCrvHkyZVwnrou8XIQ1NkQ5+LBDZeAKdo +RL0ZgnWSVStjxX9vB96IGkIastVTj7O5SoEmjNwhkdFHFZFisBQ1haZu2cWNAca2IozCqlV+/cbx +2fwWbS/hZNnsvQlSCfiOJq1pIhGAc9LcVmaXvEMe1VhGz65j4rM/Y0LAdJxBvXgxCXkc3kaTIVEB +G+6+7RaCXwQpIMZi1lyZ2LZ612uVJ1Obswvx9DzBaRst/g4yXjbzKRjjottM06+uOGD3B1ppf++j +rmI6UGbUqnC0YlLXpVBDES3mMvJGuu423qqhRsZOZI+bXI3i1Hjwlix/j0SctLZbHf/ihriZuJa3 +SRSs9JZbcrowTh0M0e+7ia17QgEVmKBdsjfKX1TafWcFbMmxtyjD+bgoWhv5Bzmv68aW8XfKjH+r +pY2CbcX4sHrwWO9lcPChCgTD53fxjwPBfhhfQY8Yb4ohwQ7m5so7ZfHM97JCg8M9D0887gIsnCkA +slCUEIPvkTkdSuPu5rQ28JyRq/EVHLZAG1TkfydRVbimPMlkuBvewqtVcYNsn26pIsDtQzS6Q60r +WOqQAKLFNhhS5pxVu4fWnoCnegKa02/KNKsnyInVSWZi3rcM8R1GITJkBsyPj84zn8aUjPtFisCs +OP312XzZA27rVH9ZapWEzswx/gmxsu0cbvlGivh2rYQvAJ+d0cyIJijK0H87Mj4LPg== + + + uY14CQ71m1LlESsQ/+Ss4sj0z3Is8Tm/P5Y3g5syavMSdsUhw5wN8eG28G87XmGnLqWxIj2HPpjD +9Ws9SHLbwwMGnxp5DwX0JVRQisGgC0o54bcEuG7GAeGMoENKOUWFe56xDSnyRDlFQ7khaWY6HC/U +7mlToAPvUAtlJe752cKe+tyNGSK3Sw0/tNkYt49hmaX7z/iSa8sHA16Iqb+guJJENSPLPxm76TOd +4WXfQG4qU7URXfq3LD3nccYBnZsh4mkC1VqMFFkyJKXDixe3EofumdfITSN1AU8Qtsp3Bfq30lFt +3F0CwmhuahzV/ghkgn/X/ERyBgaGVL4TE9WbydwUuXo+Cf71mOZNzE3jrwTeI8H5+DcGXflIEhYB +XduR8eLf7E/DGYpW149p6l+8XZO1LDf1r3bki+K65ZZhmfiXP/a/Yd9j4F8PdENn8bmqf0FeNmFE +zu08jIecpvvBiN+YiMK/IIwpH9G/zAFMkpwvCPujf6tuH7O+XxX8m4TANnWQKZMC/LvRztRM9O8O +OmVkgX9LDFMj+jqUBGLEvzGpfyuQl9h5GovcJ1KT+aBdCBEPM5MWskFWkpF4Ca4R4QTWdqqP1xej +WxBTb5L6H2wdYMEoc3139FuIyoXqe1fxdsjYZlMQ7DxxjCm6nw1uZfsGXFMrCTVUkXVu+2ryHGZ7 +ZiVF8grjFrkFq2/cLvN4yO7K6WLOrdDQwuG7j+uUUJQjNkRXTtk4Fh1a1iS6FbVkpd39EZyMLwY6 +x1l0jNOP+Q9cDPOAk+Yc4R9hvNpVqTC6WBHsU8bsTNejgF0xc/O9QBv2wd7ADA2dKHKu4tyFZESU +zdhHqp7yodZ+rePWztZiUBRqB9xmVKA90k/Wtg7C7wtIzkq5nXB6V3iqG5Z1llOLB5lOzYNTjpOv +giPPJB4K/GN7vI3zb2sGx3su6r3DY1qhTLPFQimkTajqLxVV2z8sOgQkH4eLGNKWl1zM6gpZ9UWC +p5oABt9ShAFZtje5FZiwFZLYlhAFFYEwLEpSPsU8HMRd8wOUviogGvtkRikwDnQGp2Y0h9m34d2B +yiJ8UIg902Z0MtTauXVgEm7ypPjLqXJLyFyyDkxyE0YLp7SQIzNvQ71X9z9DPUQkC6AM6uV0N9FR +76bCOm2v+4NKpKHjW4ZBUO9VyLd1KvF8Zo+gXprqniOqUKNkMaSGNgVG7DcZ1Ef0XTIWq5wC9boW +nKHGzQzyH3dw0ARMzZQcxXa9YoBYBLNvxeUMoNT4YrDxMdXOp+wenuOgEI2ezjYVBz9kxUFmTRCi +MEhsMM8pe1z5HiGzmNuvOFr2bcFz4/IvoOPE+rV8xIkXLpP5OWE1ta0DcceLcSueRB2Aa56VnZAN +4/2XCUziqYQtvFz2W4+9oGB1c1ZDafnr9x7i+IGVYdkyIPG5Yq9FXpkIUZRku0oY2eyK4ScCE4Mc +sQcrpumWkYpwUVKq+E+ceS5y3foAL3kYWGA+5TFL3aajLLdk02V8IW8MFaRY6eV0ruF+l+ihJRs8 +nFBg9sHl8Pxr9VLEMXO6QixBZTd21rop49BpA8XIGYLOvQB+s/CTA69Q/wScFy0DeuXyZWUdBefv +C326poz7n14NBWzUJksBUcHPdPS/4SIYdhDGlOB8tkmeTn4urWE7U7Cjh75N8pQkdO1kOUsYsM76 +qRhrOpwcZ36te+rqPb1QrBGEgkdV77BOn78qCQwmDmhCdayBENi8e5Z39aalzSlio1JMq7Xjs6sX +GneL2t5JVZC6j1/xj/Pw6qXXVHNrrqnoS0Gto9VEYInFMu+Vv4wp4ZIlrat39Fo0GsliFiz7f0X+ +ThYpVa16jQ/Yk65e+vwZ1zCjD6IZ9tPDqL/qPQJ1RqDHbHvmKgIjWtVbzuaYMU1pE5PGsSLI79CY +YbVdDfQnAlIW5Mw5zjABQ86aaObiwv6hS87QLHYwxyWimqmxaNmdY6I8Fu16o5pN93wRZT7fZVPf +EOhGtjLe25WIYcZBph9KnZNKyfI2lNulmKkLN3KOAYQ387RTgs0HPItYnzCTYutwjJtKdwFcUfn4 +xg40Z8jHG3LKvMDZ9zZlsTOkhufdr2Mf0uP73iWW2BUOfQF6W7cTVx25PBSr1yb10CZyGbj9cszK +AhOD+PJdZ23hS4NjMDx7Ba8FYh3j4QvIKr1Y4HUVbpLyI+CfaJdvIedc9tSEiJ27zd9yB+iT1OJq +JnKlV0o03D5MLoNJtp/0In1UGfwEiUT4xz1bPL5Syv0vJQrLqOCJDADCq34R0N9h+HpoVlAKckJl +dmUf43cgkwyen+mT5ALtei0bDneYgrha00sjD0ZV2zRwvOt0fnnFiPdMUehnldVgbHTgDMa7Cn0J +xfzuKospM98SIlEQNGrpZUeGp+wPREh5CalZq/vx8L27PUcZqedSRQB6yCJnDZbce7wLNaybEnkJ +h8u7a+pV6KgCtIdZ5yfCd6mDbZsa75L2sCgbHG2rnvsTgoRdvMsp12y9C1BDXRSYLTQqdV4WMHXe +Xa3FsN+g1NVYmoU9Pnb+7qaGrrpk4l2DL0eOgmVAXXz34Gx7btXBZFQJPTPLuIQwMgx6YAG791X8 +03G5lvDBtN7ACyPn209uWsWZVDg0dLm6Dx1pQjqM1Wdad155v/Fq+RFFkcCooMaIs8ZtbBlD1LKp +wY90RfaIdMVv+CcGe6nBoCjB0VVODhEc07J+P+Es8wKNJ14yi1WJ3Z2QRmHbiA80Xl6JCCTWNx3K +u+DgcfPy4mSU2bHj6bgUnTNha3XpYTWW8FsG6XBa7kRgivPDX2hNMluxgZnQwgOdoaErHC6riWN6 +lC0Gf8nT/Y9UpnnPasrBlzr8sLyDy/hbbvEdWZmWMOl15f0qG87bwpNwr5trO8JAHTIdmONC/B26 +hVKkNZTl3tLabsuDaGe48DYjBIDhuHtSrmJdXCQ/JsGWi6PWdyNUYcjgOd6jmQIYK6+Ps2fbr3U+ +MyAevyzDJM9FhzBRiBM9qXrF4MGjHPKcOsR/jjADn0YAVigt7w4bGBtiL7T73YgO4w+b/7LSi3Ie +TFJvZNS5xkKgJKrVGQEIwYSro9y+g54NT5E0RGCXgN6Tm+ZP+yv60737jX3s1nyaVsngL+jkDSyl +gm5BHJnBIUydNdnWlM6SRgJgLRd8ZhfDWKlKgyEXC2/Ado5BBMyKHCwic1cD4dqMDlgdkxa6aUO4 +ZEkdgC5gkYSf5kBT71fxHMAQ0DukTZoemcIlRuuOU3fSMZvoxzMJRopO38cdx1W7uJA19OO4RYnY +ayo7uiIUjx6jejSC8qPrLXwixpvnFsPWr3E/atM8hNflKlL5Z+0H930XiW6Su/6txKIbyirN8eju +loSjz/oGHdTZ8er++/jIcdFN3AqFbXawxi5doOgaY3qP7t6Jjf+yy3eJAgNW9H+a0DksRmE3jROQ +dbEsHO0x9IhsnQydnmX/YlQaYgEnFIMk23azDGJW1N3gB3hRvCAU/6ZBK/5O5ERt0QGM9BWZjWU8 +/xCmwm4MxCkQOqhTMiA2Wv5LsdUws1ekRzHnOK9eqZkCgXTceVvMnVC3UA17FUOZAHZeq49EYyha +3QRw9XM3sq9k5RFqge6lhVcBiAaGkYRYeRkp9w5HN8rV3l/L/fBO8L96jsQiMOX+E+PaCD9LcK2g +OWZ+QrQ/qQRB9Jl5xiytzE9Tv2xBdftDKDkVP3Zkkl/oI3Qr693tXYaZd9WbMaYLO4WEcpYp0ecB +TfuwJmuYjSdHGuy1PItsReIp/dOZtKU+w2jqEZXSLziJCm6Au1uXWd8ImUDdpfCW+Ux2KMJYCXmr +FxG/bBTs7k0+XxpxWM7CAe0Avf/46mRVoHm1RSErdUrO0AyFWNdLWIqnbpwd9slm+X5YMzTOoHTV +XyZt3pq9we59gl/xgSxAULr2mOoKphuF3vINXx0On2a9VLrlqIDpNqS0HRqIMt3NQpWnMOzY00Zi +utv/dwt2I4ilasTzjE05Sk4WsWW63j94njzIq5/ADqbLTBz3MvdoHawxXRZAMEuQkphOKN2bEFqB +NCKcLQYZZh59yvteTO3B6RogOCzI95XqEof7IaIy3us1rIp2MP/jHLWGPvGg+OyFxEfErYxMXMYg +8TrvgKshWeJgvJA4UqN0zqbDITqu6vZr9qC21ZyZ1Tex6FgZ4MjVvkXmrrizXI54gfyd8BBKIuQp +PhHBhl+9Vc4bXCrD/cCoNnXA8j1QQjZQ917kZfBQXebz+sRqZFT5gnmlkcgu3X9UUzDMrINz4pGx +5PsrZMm8O7maltHkwqp5jO5Sft0o3ux0ZfvWnVQzS3iPztgjJOkuHUWPDUmGep09zjDml3fxI+6i +6w+6WBwXEuWqtGPaOX/gxfo+iZP/v1eegnQpGGIN2O8Jm8hmrbCsTF6pARTgg2fSeiTtXlJ2v3zO +vcZyT+qD8Bossz2p8M/qRb5O+Mdwyyfe2IUVC3KtanWTIROBYuOtEb5AISBfRSBFWK0oOZuh6JDI +hYZfQU5zQe02ID1CHm2LbG7rJ4MoiwKBkmJ27qvxN8+1hwmYnyj36ppnoOB4nC0Min+oKgzzYp0F +QdnEaniIM0py9qozmD2uPHzP4syXrabK4eLZyCk6e+TnWwkA9opFM2DTspS+cTas+Tz9rWR2ukgI +Ow1WTcWyl3HF9veZvuoB/vIqQfcuaUtTb3KuOuzopW6XokkMqmLNv3CDJLQaVAocuFqKlZqmgMmw +qOS8xRbQJ8qOaDpD5JpvDJDquFZEGUNVjzQu8w3NoTES7I2ALHd6clYKD+CW7/kn20dqMLuemR8G +FztgxeVBoAa6ttk3KfXIwyE5t/lQpNk+0TDkXOdJznlTUVx2Ft2h38WPip7Xkg3/eAWro81sV5qp +e9ksGtpfRLZq5cCf2rLkrZLHJB3ummt6Gd6uexx+9MScsbZwpvMq4EeIbGZjP3el+AhVu3XovLof +ia2UuFe22VVMw/AYlolbpHbmdv83XjKbwv74XXoTIULCZ/zbXMCg0M012enuUpwjNpQHBoPuyhFL +nN2tQrBFttVlvgBs28TgK6p89RR5yLvsxXgRsgCyRi0dQxl/ps2zszgqjGtMyVqiczpSklSgecHA +d0uqE1eB12QR64tQZAkYgwiD3ZDd0FvAosjRahE79+L8+O5mLjWnKG8HQERdsXbcqzU2yi0lqZ8R +ANadmBUB7i+OcGev0AUGu2KPZW0RQMD54WPOOU9TAMF8DyhW3/Znj7vua8etz5tuTyzutHbd2nMw +vpHW9YyUtr5Tvj8FeFOxvrtms7f63jdvk+5eN+DkRkywq+o57Oy0JXw7oLMa8NQobHpRDM4fDL/X +3dr4/2EQIcMJzaGyyxlGwwS7oALkjEKHYNx36REWTgVGKmrHSwF4RaoXr6QArqgZM4O7c8oAh0Lm +3l+PTjlhX90zJLDSp/S4lfx6tdkbfpkmBRsSsX0NqWaM3cTDXlOZ023hE1x3JSXy9A== + + + hEuHWsllWblLvXaMYQ6rwlEwuHB5zE7TFdZdX5vFcQgO7RZM+2ZIditqdLOqfmrNU/Uh/m5A1/BO +78prTGHsL6Dvwj3+Gyd8788DiuoXHk6OaIIsUYGkJk4qhox9inVQe/wfXBUr1odX1beCoq5mLKUk +IBHr3MPtbMRimQScLUGCApsdUhEBf0YxWJJxZbjJTD6VcEtg2RyGdaC7YB35vHz5ZB7W6h1xfbNg +ogWM6Y4YFZep+hYSvrmB2/g8GQriXDPI2cN6CtDGZ1Jc9K4wI/bu+E8x1v0IaFC/qx+yP6hHdZUX +opywF+NK6C4i+G3v/QXAoeX2IIwibULJDUz5WRo8na1qQCvgmjMV/QMyQivUU6uDRafCAC5a4Rm+ +WtI6syNyC4IBIJn356jIThbbjJzv4+zybnP2EnMqSOFePhzXA+MCeLSexJd2JHQqi9D53ixO+Ot0 +8qkRK3uE/MZCyKVGFhMZEDwIyBv+wyf0B6oRGROomoWWRRqr8FSSjbfbTbzNlvRSwS0AphpRqnFD +92W0RqUDM4JO31uVi0OAn+Ygj07xRM6qKSYdcGBMZvFlCNBWjqJxNhuqMAyyKjpEWnFvVF8ol/d7 +k6D6XmTml85XmBl/l5lGlt+evNhZV4AwJKCsFoqi4iej4b1WHlcDjt/RcHNlHvLgiuPzzUy8wc21 +yH+0MxcIcEtzhi/2vaufvOlr5p3qwPzPaoBdrGWmu5r7d3nW3z5LxK7IGJNNEvbj1aIA8wsxJyD5 +VhH3pV83kcaILvorWzLA8jHC7Bv64gG1uVRqqWFoZ8K5sGGoN0Y+g+EPOuqwiyfPROyG5WQ2Qs06 +s2DcRICQZwAUCRoMgJrycDsJPMQ2A3a1WX1e1UcYNc4MKkcHhNDqFHQ6UCLYVOlTWU9yaqnLfqZe +2wqKnhycgcb22/I8ahcHDLao3ZbJoHh/euGdwt9aligWW/vDc6sKBGFZWcpj5S7iejsfqTuZwBSr +fIJGIlqaBg2j1KAcx2HkZ5rosa1YJO1Cy2Q+4sKNe7mZPDrDIHiZLyKjNl4QkNnURJl/wkEBKEtz +bJPrxnrFv7afsqIz9miScolsL+Aqxz+OlmmzCTVJ+EcdKmfmD1sEDFVz1BAx+AGSysJp6UGMAV43 +LvPSsEGuNU72kkAuaXHvI93B4ruXNiuONduZzkv/J3UCQzCvf0lGvjdV4tdkiODAYYXqygYJdCXj +YJhLjxMn2h9aVacwOInPCyXI4WURKKn90YCQ/2mwfh/2YqgM7sRylnst5Yh4M+/QgqvcfFya5hGn +dqTzrUWKJI6j1pzsr6u9khWKwBiTPq85soBwM56CnTyTm0XbCfxrDI1Eh3koLzv/GZX7Lq7gXLOG +iLIPYmXHQqgECccPS7yY6sbPa2qmjqxm2GAr5Yuf5/x+2PmY6MtzPbYOvMyEFBHo+vpoR5UGikDD +/ef65qIxOyjVWYvGaR8bmHBSNeWmrJb1y3Ej97f11MiYHvOjYT90lW4xmHOiCHAxaMf/JsbrmIEI +M7Gwxoq63u9BdzeB3nJTH07Ml8p3/jqD3XDZ3yht4y6p0oDMPBFlclk/JlgzQd1Lhe0Wsf2qgC43 +0yqMGwLqZqaHYxC7UByOGqEspqD7r1WTmcAVZWC9trSZ0Syx+2Jf+9/997yL7ZCNncbSmJbayqu6 +uebfVTPEW94TGZOXUSkycZs5XTbN4oXipwy4O+aMGClqLgpvQkAEBM1gjZ4TH9BjxCN3CfQgRbyN +ACBfaq9yBPYZTLonnlGJmkXqfQ9+PqrI3HsqRdMFUZ6cp+HwDk6qQVsJ3NdUOYqu5nUbr7r40rhp +MjBrTr70ln1+GeCfYersoTg8l8HgF0DQq16SsenM/iVjW8b9vhaC3SM/7Bw2gCmZKQsHGOxbKkUW +7Dfa0cayu+8wRgTIBrUPC04/brCHjw0CTG6IiorBmt+pB/oMh121Ywp1Z/CqqFYYpawVRmaePQV9 +KM1Dn/di0Y/U/e8xGE71pZbOAaMfBOENDxWSeEuFrGF+MlsnGLVz2fXpuTlTW0XUFr3iZDiKkc50 +8rjQYaFkX2H08xji4sz1BJzZDnwTet05FRyYRmK61i3N94dw7ezQvLzFX7ESdi1YahUYru840XFm +LB2wKbAaL2I7c9iXRibGqXTUY487ITdc7t2ImB8HIc06kys8mmCEUTAQweQZ8U/F+H2SMhAe3WDk +45nRVWvJnVX13p/2Uorax+NeiUkgUQDvKZinvu4FBItlfPvpVAVDJZViNjzi7swtHQxUuss6VZQt +N4uJGgZJ4eSzzkiQFMwXd4W3jywjdH0ubOe4q4tM+aRg3GQ+1IFlyZgAsuX1wTiEprbPDIz/bIF1 +oQ/1CIzmBQJs13ahPdghrXFJGxyb5i4KS8HtMDzuNADdIiEfQyyN1HibrPzT4vjyNekConqZMuAQ +dq+MLji4dP19mV5TaqiMBrWAKF09kLkibMXqUDlcdjbYtdIllU4e3qqfkUpOqdZpnIEgCHDGX1Hb +pxUcdOMLb9RqcnpM6NvK0+UZAEdJuvDKz5R/XZ5hoq9cgNGThCTt3VRYfK+B+LdgEINFFqCaYYPx +wkPO0TnspBMvilsdAXdACt+JxtTHLFTo4XrM59yAw6dDZlfiS+hzlhGkGYTfZvTwtwE43EilJhgH +soTUyx89awke0FwMmyCdWi3b5qMLxvph0MqmRznh3avwCAOrIH45mnjm+AQT6GpNOAjET0kkUl8M +LQ39ynvDT2lj1rtfCNHrd7SosfYaZGn96Tv/p0fVQ2Jl0XcUfuCSJerFbPDyqUbj0u3KH+tVw7oD +qqf00sASSonqmzLDYe9FuPDUFEhlKdR8jzr7Vt43JNMXkVrzlM2fYhC+AN8JwJ3cdKJTCBAbr/o1 +EEy+dixWkvz32CWcDBXffF6Y4m2Ppnmgw9QfPXLRdUitU+0C1d83jyIcE22kAjyGcrh06+bGib4A +W7KDDte9l5uVmDv+EjbXbYXA+qzSCrW9OC9yzXHJwDuxEyC25PhvPJyRlq2lHPx4DIhfxmQtSNQn +mIsX7ARxm361kWQipHQyPqRoGbtG80kZmtzru10G97bwNR2D1ONHJoU9byrjgTvGBO7InptTYFYQ +ubdh8NjArfhzuhwq2M3037W0497Ukk1nvoBRDWstZ/8RAtGgCzPTsFjwB28GkhNyzL+g0JrJLetY +xAiDyMNiQOnnl9JmebSLMPBK71Itu2+AWsS4OXmin330J67CD7KfiOXwFpqUv7MXsUmMxRf+GMDU +ZwGxo0VDcZGIi7OPhd8ULYk7FyAaVgeoZbiul5D3ixdlw9b+rH4T/cqDYSjghFX2+xg/0zIG9qDW +fnfZ3vBVFbr9kJuMHAlnT0gJO3caO7m/J3SDogZOgAYVnmgmZlU+Qe4GBE/QlBJEuoZOLOqNsPJu +gIIEbSYK7ayTOIUy3wxcvCN5SO21qHB5AktCLk9nR2sbcFdmsRNcUWvCPPaC+zwmftUJI6ZrKBXY +H7epSa/wbvabpPfHwxo4BidyxBtZ//QKiR/C5YCjOJtLeJJ/R4ZWE0drqhC+7qzTTSNBme6NUwod +GfCzoLOCdhlmAbNURe4oqRCkWhf4dyR8SqiaJl11oTo/XVu9xyG7hEcH02Or5PcuMxk16ugB3Wa2 +O+XspTLnO/8mc4QAgwTG8vmMyXP3vKG0Cz8KaOOX89LCBUSCF4qKaFR/c+v9fQ8S6bFMtmO7gZbS +Vk6ELpkOmmFhq0lk0px0vIzz6zc4BWVYLLcQmEGzVOSCjTbe7FIR43EwTnr4P9+ulBkpD4uDZ2ye +akz+qhDyhR3xPN55fN5lhLz14tvnVaSN27xuSuAo/nsJEUWW9txsBvaY/22qixy2cGY+Kj/VysRZ +C8CahGodC9xfju2/NujG1Tz28B3BoGeic6E49wWSnkfzbbofYpKEaaaU4X3vdHvNzP8Jh0kuYe5h +2t4SlSA6xU3m/Y5AEb4BEP0o8je0NtRTPm6qV+QBQZCD73eSa60LB2NBRvxraWuk04tJZcQ658g5 +Z6y/oaiGAXyyi9qzNChGErtul3/w/SYjKg01+nA37bMQTXeAqvKjOge6bqEkgTw4+xJEFCNITKLE +KhSoo8qkh46nL+dmeOI5CB84CMRGh9UC4cW8hNa1uEtsSMKKK+/plbHpdU3wFR6tommQQ9QCyNF6 +u4f7eu/SvAfXy5hGfSl4lZu9IFRM+30yqGbni78t/k0jS6pjXoKhkoHqHvi3GtRpGw27LcZMj9Jc +NsfSEyxpQL5fyonl6hi6iWPdkxXJ+CKpLcu81BlfRUg0a9OZoZY/rFIFqsEwAXXAzWFiIvj4evTg +Sse9cEkNdTXR6KPDdpUX0LJAXVCRLUynGu0FKAiOBJUuLmEfAOwllrXPIPHXKtVB/nG27+FIH1SC +VX2+Eds0H9MsYy3Hs25QdIpp1SsY5oXcdixu36gboKN21GUg5FLj2yhr6WCJeK3a9b0oDhMIYWC7 +R60VRC5L1NbWoErx3xTVARSIhBMbaLYXblvDExQApxQzmySRBdhwqZeJAk5tJotv+vp89jUBGT2N +ZPpiq+3HFOQ6/RFRuSslZC0HqhMCjgQ4/3GIZXrNY49aiNNW+dh4XOwk4mv1MjZiLKXOVRrYYf4N +WUe+/Xh65nuAmSKDNpe59MCHeUrVKoIi5Sh2niIbmXRqH/aqvYSa1hFY71rvzDd9M30xJ2gL1hjU +NqjQq4w3hHsXTh1IaBR6Bze/+uPw1zXQldYHRmQus9Eoj0nrZcMKQwyXgMTIiqs/vko7ltuCfWAs +u7C3sXZF8x5rYk2DiHfGaOUULxwwgYWt/YgD3EIzW5el7YwUBV3N4Ta+xuht4OVrjL78xBUI0jk4 +kOeJ4xYyqt1LsiAc+VpGiknaxjdRba46d+MFqpUBwEjfz//BqqDKdencVTNncdTNuAJieWdOZA0V +vszYr7lPMd+AQl4xPQD6IyBii8rThVXthLqOGLiB2fiL269AobGULwM/2kjGOZa5aAJD/y+RpWGI +6YDrYRjhC7wQWSh6XaYB+QGzZUsQjEAZcTu6GMFbhb/1HwlAD5AIc6M42E62erzg/qVVh0JJvQy3 +vIiUoD7Sgem6FrBGIamIn8HOaGihNiNGmbLdUTPFapiduG16ji2rzDrJZ39QtX61rmhp42TG3f82 +3qJTnZcbWbmux4vSMw7LlAD/+UP/nwkg5u/ywJ5gohVokswlcBOCrAQyiDWAfcRASMQQ9CD8cOgh +enCXoESQHTGFVeHLGAlckc4WpCtpyVUPZTka57g1vxwHrIafpfvKmYJEfr7K+zLCbLoyriSsXUIs +0bJaPLZ9H59uP4ujO2G4RgPpfpALDy8iAlmuh6Tw+kvt/dDQxSa15TQefJQgZgq1mvyndNKuoi4q +KCVeEXY6E+9cCNXfZYo+agMYq95vGSKZEHE6gUlFReqH+EAYHgUDgT+Ny/hAkd6h6A== + + + EkDWfCXxIK7OJ3FKPFsvHjHc4cqlaioLV/JQFsIAU02pIoMusuzjEa40t0cIU+jK2sXBTqO3Rzjj +XzHF9OudUTnBDW90W1lcWck18CATErMby9TJfxN+CPndOU3g7vvxiXeyuPHPc0A65wpamIuA5o9v +K+yJX5KoieB7vbWzVbKV0RWpGSev/YXtXcjPdQi4f/AkRRE9F6nWtM2/dHBnUa3YfXfofcLeqpn/ +0sj9SOm/e11FmCSwEF8z0r+n3ckC1hCirVRqaWpP1ARq4hE86LRp2gjbXq7TVfKyEaF2gYPrLsOX +ZSXAesk89+5RqJnmJ7tcA4psQzGp/R6UW8UNAlLG+J8wzb8E6OTPrKA/m4bdCIUCAcr4vB4dQDNI +ig2DAg3qjje/rles2JTd3TtlWUFUxLCTlG/bAdkBzAEj4tswQS3iMIxjQYw4xiA2nXP0HBltZcGx +oce9CE4NXTRllBta7jaKNjNCoUeLj+Lg7xdv70ehKqFgF/NRweEQ9U6DmDmEgm1ilpT/nGUmZS/m +KXjchSWUmhOFMLg8XRhlfEUhFF4UuoYiV0cTVT80oVBXJB9GGLXCsPdXoRCGMJlCGIaOB3s+fUTy +0xPEELvN4PZTNKEwrxpaaEIDJWLa9NlAMjJEhRX7IIYfdDFkKF1RekTKaIrSZNWQc7EDkYstl7Hx +unRVmT5NGMq7qEMThpsxSyj8QhPXslLIjicMjaFSxRMK/Ej5ijqeQqCAosSnTJWtufxAGAgF1yq9 +s8MO9LthCQWyDWt3tiSqLKEBsdeOcTAVxRAZEZth3CAGSRAjbkqMwRhX9L0YR9uL0Zs7l/zGMLUw +pCSLGsaZ2RWDQEG8/Iwdo1DgXfnkeOZRb8SztO5uPDP+YPzDOKEBG5thXCKIcbsC2fyM4fIQ+/pk +5o/Vi4rxgylXixK3qX4+9yeKH43SHM9kl9O6NRifoWaOQ4HNPsG4FFgfVjU+NISCiViWsxJSmcsa +ZFgF6+9FdB6OBFE4YmosuzPlo0zQbHfRIovPOVRRIpqtjxnfYycPqaD5YOroUTR7TBcB1SrtCi9O +x1PDqbUXwimjl184tZMKpzbmlL/4+d6I+KFeNHx79xC+UcH3cjYUX6u7uLKYRvnW9KOr6IGmay+V +5/vyr93EyunUrqzeGMJR5SMrCiV8Tks47ekdKLiEcVoYKDAhYpxu6b69oWM6E7pFd0v3jtq7GIPt +wXjMvsKIPTPGPxbE6BhJ2Y7NYJ/ROZqqGYZVndFBRsKM+vy4YoyLPrCJn1GfUbmjGa1gDMcjFCri +fhk1Rq2+iJpl2+AJDXRa+jov3ZGIY+PYEga2BAoYB/axfESuZcMs6PIYj1AQ47WMYXdEDCNhDDRX +hUoUr6puhGcHj9DAFdEiShinidkwMrxRYhCGEwrFsqPtWA8bK8JAMcQQ64xVtJP8aDyaB38OCp7Q +gFj8fHclN0O0KYhX/9BvGydFwiMUOjSR8GwReeVIrZQ1eIQhajjWcrRjTeUFTZOFLpxpKd0ImtCA +SVwUeTSlEFbqvCMMRnX2TiY2zBP7FHtXXhVNsSc5GmGYYgkFRgyNSjFDjE/UfYpxPMIQZYMCuy3e +EB4gDGR+xPxpWISCTWlYUS4/JMMiNEDhsVXrpH4QTe6wxm7oocmuYZ20aAq5yyIUyr3aDGqJRL0i +jc1QIBLFNEgnJQH3YN1+ToGYEOPUYvxOdsanjBMVb+hQFeiMZ/KUg3HLHc8TczeF8Xw0O2o0t+KG +4J3N89Fsmmv5UFqYZptmshgxaJahuZ7ybJ6PzLPFeRaezybEsw3xHEXmaX6fVtU8zzyfFM+l4vkk +jFcM4zMVIkUzzQ8hY5nEWGb5HAcZhzRDSSYUSvLdzVzEieH4QxWHM5aiz/AafXrm2DecS43DMcvx +oa66D0sqjmUrBc00U3DTGiBQMFah+EdiF1PBqYbJ7oz5HIPQQGkfjG1o67wWYRENOI4dhkJPxdqM +ddAXtAkFC9p2xvqOohoGY10tW8fwMXxXnVkTKJibsB6OrhiuYrgK4VNJMfywHbZpauRn+FepiPFs +HAwPZZhQsAuGW0IDvV7sqkbrjVaKo5VaXmOgCfNKKFh7Wk2rBI2IaW1pvUurVE1qpfXsSuvEaaXV +7GSwDsPDMMNFDD9AaCBq2phptRDanIU2ph2r7Dys3i6s1p2jCt2wng+JFaqQbKpOatVajJKwKdFP +SC2pD1Iposep1Ks8TM0Vpz9OZ+xWn8aGdpFKP6pIvYWRGneklpfaarNqVn8yz8+cWS0rX3Fbb8fP +KqHwslaqoHWMcXCbghOpBAMOKAAAAAAAgOAABAxYsEEFEWBw4MAFFWSAgQMXQHDgAAQMWOBABgwc +OKABAxhAQLjAgQkkYOACBAYYYDCAUOj9NTOVlGMr208hxRMmOOjRWYvGwqpjV/WoHgj147ppkF1Z +iNaDxsSZVRyvxxQFlZOrIt7B0MkjprcUf1A08ZNyLBa9s5GiU3SkLDN13XxsD1pWQw+hGuP1oO7u +v5QwwQFJxVZD4Z+IfDI3h+/q07hCMYBQKLvOxe6SMjIhu1OPNZXLelFm1ahPpHWEe2PEqNCM3N3Y +wrdCYlR1d1YrqvnSelVVz6VolQ1J3ExuChma8i11kd8spd6dzK8oAQc9mCGpR2PIgr/zzdyHSix0 +kVbEqsiECe6oyPexzt1ZUDXKUyRyBzZkuXvsLYZNLOToJA0r2dXE6nT+UdiUnG7iHFKLCtfQzdRW +ISFkrZgbH8RBa26zaRzQUIyQY8YdCsbBmTRu5CQPmyOTzpxMj2QAodCa9TajB76as99RtHypFxE6 +MrJp/8SIbKJOhtyql+dKLrJ71gv/TErXD5nDj5FzkdVORm6nwAIHAgRwcAkMw0DBUBiGggtDYQgK +zPwLoss/fbCSyt91QSQMhzX8kwwR+xeKfYnBKFtUz86RNRiP/sP+ccV+oE9VGf+mjZDBFkb/x55Q +EGJM0Beqln7tSqYvc/oM+i+y324e+zGoAYnFqMRyB8TRLKsVaRrYEAATEQAQACCScDQWjoel5vYY +FAAFWSIMNDZQVEg+LA6R67IaGgEIAARGhjYBSIH9TZ104Z+SUGeWnANf99TSk/SqJbxX/bLobtEv +vWiZkDpru52M5VIeqC6Rj52PmspmsfpvtRtVLEhXzzK1WJQAl2iaGWjLJy6KS0T8/piIKYVuiB9c +lk0+Mx3eZCHVql6nleRXuvOnJE8LEQQHFAH8G1uW5/nCO4XCklLnGm08g9GUjK0qlUuAkxhgepH/ +lQnofLRCbEsvIjAU0QBDi6FMiXBoGP4gWSqDnF5y5XKj4lAQmegNlQhaPgIxqHdB0AtiqWJ2cJmn +dfxoKPDNtXiXuhxtm4CRaPCyf6TUAhGIvo01e3SpNaIUwV8u4bvhrFHV0SABWt4hYmRQa9Ismewl +xwGDtGtlmBqJYYEx0QM/0ivloX8+Zn9GRKeaHdBThbjBE4hBTaa/qSBK5vwIyI5OptKOruQF96Ta +J4Dd8fDVW6nOHIXfPTH089dJBWjZce750aUieaGThX+qVDQND6KY+k/771663soFw8njiQRRPynf +WRD08ga9bSyVAMtMf/FR3jgBarl7Nsvv4EyeWS+/mkOQNMS4H70mIpgVBESXBDRUxQFrOFRsiS7+ +bCJCVN15zQI78sNqd0hrvfGj5h8o+Qq2zrppDoJc6PxSeGf/dC4bncWY0GfFUtGQXMEjXJ+FJPM5 +t7aEsbXoeVRqbaPSWmXIOXKa7lN9GCwy9WUkNVUHI5p0VmI/8C9hB1TsO631m0G/9tZtSa39peU1 +r2+g9LkIE7sU03It5hA9P064fLJxOaFceTeevYotWJQ2auE5DUDLALn9DCHfLHtRLuyeB//nDsvF +pF+53teVG7aVfwlYji0rB7dd5Sc6k6qqH4KMqKkiJSoQn9+JihGPIaV4OMBST6qfwOHqkpPXKXRW +DQuR8tJl62Lz/qUoNlYE2HmHiVmIprYsgLNad9KWJAb4c2ktegEnbVGyztJi72Xc0WKHeRmomgiF +H1CnHmaLQtmwJN87DSm5kb79+yT2eGQaOlA8zOY8MQVQMbOBbmWVp8JkU5Mw0fiNiQDzUr7Mx1fl +hmt98ZdSWRWAu8WZ4M0G8EQZHJVJKMiQ8eLXXborgXhx6NYXmJKXXVgr0lHDMLCizgyC7Qx9BmpX +spgLCv+VP9C0lZYZ0gxKo6FOWibuAiKaCRdL8LA+D6Q21KiJvM8GNPCRVWezalmhcOIkUxZ81giJ +LSpcfAA2hFwk6QL0asQ3tpEQs7a9xWrsjcsOPp2hQNK54CACpmNffGqF1aJrhxwg9HI2Dnz508q0 +loIVHZitMmOIcHNItBitki4mp0JSJLmGlyZGheC7IOx28MPCxFggL8vmnpAqbLi0BlcfICOZQbeo +JYTA38T7Gfv+hBv00Pc8G7Znqhh7Iq3JsK+wtFXeCvtCGwP6uCQ7c8j0EMVM0qSMvGtP5d5aIEEf +y4AnThalSvLkauQHUXMCGWfEt5vGyvKrTKgRNGf9KMZj97LsYO9hWNVn56heMoxy30MkSGHMHx6l +xKGb5cFQibICz8AkikBZXM8rF7Tl6uVt5mol+DGv/lscITcl1f6ppBwy6szPlJ4TGJmrVYf33B5W +NFFqhfedzXpiAZTj98Ew+aXAidWrlp8Z3u9CCYruBaaPnuEDUTozKQGzJqiTKxlCASCMZ/wgVebr +Ecf2x9H0HI1EF4xwoZYo0/MuKu5h234xjRM1VHDd46G7+gXH4Va+TxQJlNRL2I/MmunpWGCBOGQk +tUOMUp+HU48zeJzEZzqjuBwxnJEEUHis1QGKG5Ou/qSZbHJrr2wSABvRAj5Gsi8Kc7OCMIUZ2Dro +6SRYjZBlllL0593CtxZhAqoWjAP0fI4RwQiSVs2exAqMMpzkKn9llILvUEZyyy8+1fU4gDKNbF+0 +LtVZWzXui5wdZoztX65mht78SHhIVvwiwyN6yzVvMugbpa+awzEbDOvDzf0mPeopuJE1FUsPQodX +lNmqk05WmTi8RbNe0r0CQArzts5ynAmGsVZ+IsWr6vUjStbGb9sUsWZ6C+jTX6ACoc06J+ipLtHU +g6V2n4784P6mKl8814+jD7HRTmzV+bTehREqRxy1IIHDZuSkJAFJoJmD8dtLHBcd8JwA9vIjW0k8 +WqYYe93eqBemEy9Q0wt+d1uiq79kwfCkq0YFqaliuPTSAbCDDjKJzfbv6EHVRucysuNHz15CBC8R +gEblS5vmoMmbENYDEqVOnJTNiIxB4QZdX+S3m3uPaRIO6Jjb9YV3c6LpykISKn0JAPR8N7sK802C +HY+b+Tj2RS0fdzCMERAlkVGfjk6ocJAA+9KlOn1rNqxz9UH+cuJD85XhiylY2l8qQUWicKGziVPJ +1GCBPxssh+aKh7+CLRfcpSuzwYVAG+2ssCFgsGcv/AvaqoiVd2rplyd4LPn1wMnmDg== + + + n16vFdnwil2SLyvtzkOYp/rQNz/AE4O9Cs6we4jnO1/y1PV2sZzBihxH1XfPpvFzQVbh4rSU9PN2 +kYfTEeN5c+KPCeMX3zfdqPfwvIbxrDQ6wdD2Z9CsNpTrMMFV+nzYHGXvIho252eNFRs4903hS5oQ +bj99QV7mxFxF6/0pj5+EK6MgSoBHjSQimZpyjhdTlUyZsjAlXDbCUuG/LRLiicPtEkEXMOdq9wog +73PESblElkyRCCQhNtRlbid2J/he1thhLvjvRb5xMCOKTfjijIHONLiu210YGsce8zZHDaZ+7Gih +uNl+BJw3V9LAEMbOirlERkYewbNB2fM20SVflfOI2aKQ3YpiN8qKaW0Shgbubrx7KY7bHoIvJg2B +a9OXuq7FrD+sUnAptfMo5BUqMtxQoOf3XvAnts930pnxTTz2X4r3B6QISTozAw8FetWfoA+yO7FP +vEm7G1swki5mWkdBkRKIzbOmXHIXfcJFR0hYxsiyoLj+YOUVS4OLtA56RCQZw59mzbtEAuq9j1jU +b0LTcGgX8dtO2BZi/H9M19YJWAVInmd/NKnFnCfbkVLoJ5nfsM+vnzM7MfEbiRymrQ67qP8Zlxtv +aQPOFcqlnp/azYMFxZfSeY78noCTgJCnKwHDEGcPe0rad4PHm22F5dxwS9PHmoyXbkeA/FCXc5VC +SDaqXSFnAXBXPLSh6pOXY7dWj82Da/eZmBsa4sfAR4BnDhgTimUDY+SGZHMjVjSSh6DPy5POULnB +BMT4cpk3mBSWyZPTFfkR0hsTP9xoHQjZYSwUQiI81we0b2K5vgnRvLKa4rUfpElRfpOyuCoted6q +hFurSKSXVUkFFpnSN7AoCQjWhlvzrIHVVFR5hGlVAZxRyUMVkQlQZjUoYAJCdwX8OeH68btILvew +9Bnk1l5OHfMihK0Cl1idhIPBYz9Lh8I5We6tA6zytUR7d09Zlxg+shWcpb+iTHqJEdTqBIqtNPFC +hIO0+TMdVqzE5G2tMF2J4M8HqBDnz/109UrcvZSmlUrnk/icC0tmEvdFyShgY1qzkwbLDHUjMR8Q +gGR4uDZBuCBJQaD4OYiQiIwTKtx33TRsXMblEW3jfJscCvGUxYreLvQyuKJA2JAeKfKyudRu+WDb +RKW8IoKbK0BVjg0jWgDtpa2iCXayRPipmQolzawnjXl7MxKd63mnixttcsgGYSSFpb1PJxCxsZm2 +D9QhLnffmLHaZ59jfPoaHNHM5CgFp7VtIbbQ1rILkesWx76M12Ye/SQrQ01hB8Pw4Wvg5QwQzuDp +RxkHNKYQAywd9hRivOQEJQ3WvpiGZhgnzOAsImyV+Y0TSBlyEEeaCUS94ViDBpzDh/0qJFOyK7Xw +yNEenhIfCk0tVCwIzpYf0+sBxNhP0ks36F9UJKPhIE6c8wgwZKpDPxeivMfREPGaWfchL/Hd8T+0 +M4J3lBY3q/2wtI91Rp5TAuTThk2rdgZtuCDAXgxBnzIACMDxnfZHiSqhKmYY/XxaOFmFHc4uOkvD +aP+izSKNfcLU5ofsOPdySopa1jaWyV1wkj77PXlLWvu06stkdNHqTA+uHAOaLFLjwaA3ImGbULKR +/tIZLrbH99Vghnpazm2zw1hsDjR3R07+hQ0upAxGsh6NWM3nXAkrSpS3WoAwIoAG575cSRXMtHTH +oJRh7u+unuFYWrIBaVZhIAQiqM4ZFLLJJt2rkFEDEiUMDSd03Ty4Xs4Jysx7p4+pJOj9dZdeTkcl +mjnOh4i6Ea9a4YFiHRjQ4KAGyz8qSAyamyMUIFLZoX8vibiBNTFiIlu4TPzzliFbQstN0rZk7tkJ +h1Ki3u5G0xKp0ZNusKUtdwPDE4WU8hreLBjstDEI1oZZLFf3tU9WMFtPxf3BVCMOgbp8BjdZiFGm +DpZI9UO7VdVASr4yfD7zSxwQiih7h4hcl6w9f6YZekCQo4udb4sIL+y6Lm6PKHbSUwsqKLeS4wXO +21ARmNmZYwDMWEYIHK0SxnSVraJrFw7sUKBSGml2r58g9Bt9Un5HytfB/SwM0K2K2CR0vVkUP9Za +XCQwBE/lw52FFjPh3rbEUZlU1cn8nYNJ71c4AgujvqrljAlTI9AyfcFq0yyJIfSN547lwDJeMlIR +2WhFUtUWIThr7Eqo0AhG93G0IHqgIhkMcYDa9I4eWHEHCPeIAcceedHCQrYySdwh6RE3n6tqqPnI +82E+E+DxFC5YOqK2co4eluF9eiyzJvzt0P/FMNtZ0+xIPaxrhdFsZLcZbEXbi5lb7sNJdJ1MZp/j +CdVEQAUyy1Nv+Dyd2DM1jbZAQcov9Jn0VoIB8pvY3XHD+34gWBC9TnNPYzv1kAtxIfIOvsBussc5 +ndliZ3FmWUxGehacOgvY0NP8ngUIjLmRQlU/z4O4YaAn9oE0B8M2R4wenJxTVRk9oXzgfTX9+chs +mny1V7Lrh3lpQ1NFQbNXDkgi+YYFe3w0fQcVMOVG9qPRMBCcW6rkqL5OjAXKniVOSFgufTccxAgT +dKjPeVIjgid3OY+iIdiJCESb8PSXKsIcaAsKLop2rpwKmOH4ozXP6rULpFJLpaXxt68M/5XASBVs +G1UtFYZCfNAPiu58QHzrOkXv4IT5rn8fyKp54OzdLkQHp5Gk3hLsAr19SFjeyS2bZFUYyKqk46Po +XgarPwjamS/P6DWcLybRTGzAfLFhaHb5+SK6pYgCsvM7X0F9trk3kplN0BJY8GPaykDYAQkQa5+J +dSnDC1CY6E5OTVIZ2crM9rex9tkg6dZFODC/crHIP78lx0RPMKURSLFGQq0rwxC+FlHgryBaTR4R +dRFNXmeaT7bxp1II203WhMVzCOig09rzFwE8RY2jiiOBaKR44voAbiDcsXDbGVUEHB+2MkAS1j0g +66lpBPup1U6tEw/h8k/w7GQ7Q2HMvlI9TvJ2cbep12hZVPakpxdreXEcnFiODXQHkBCHN1pxKTf0 +AQ0I9SvmqARq1PzXAknUctoR2SpqSeKmM2ndgcN9Uukv5DbTAC5OMjGqU4QaumVhyGGKauNGm0Mv +YQ6z0kEZy6J82i4K+cQ5QMKht6W7fwIwgPSYZosUFEEGcKiFcEuCvVoGmXvOxeF5LD+JZzMtbuLZ +WS0yyPaia4JTfageCuuC8VULOZlTudYieVn0KyIVAP9zCKC5I8LmBCv1maeBVCVOmon434KdKnJf +IlxQ2KriKQ91OFudO5KBe6EJZlkJFo7WrRhZ37Fv4gcVbNJ/vSk1LWfWMRWS0AmkqGjL/hol0rGs +8lICh7og4T4og0B7Dop4ibzZwhuMVbCLSiq76wBstKLa3rO33imHLIPkBQqoaQGavXSDwFVwCBVP +3RQD16wI5X4l9FxXypE7aq1eay6OAKBNQm2EzvC10xi/KGVuFcdP14EHOUTu20rXYmaKHCo55qWQ +9bOKYvoS3Liz+IMUVEMocviowW4IU3UWEiT4MWwCum2Eciaj+QKpzJ3TsOGQxXFr1BjCjKI9IivU +xiUAxCsF9kLWd8fSsbbmGkFMra/T2cRNam+dxZRbynWztqwH9D+faTx+AlpIQexy8SEIxvbuOnz1 +iUpGpswOb2M9Mqi+E5QP3pDq9HqFao1iAQQT/2qh8XNo1jQnN8UhCSd3HTgaXO8IYzGVy5bnxTc+ +RF+vSoTDpZ62eaqxsBkWjr5ut6ECQWIBqSybbG9QxTqDT0knBA5FI04EBtu82Jz7i4oeED8XEdcG +osEqumnYSBNOBLBtykmsDaACUEe7hjIZhrSHLDZaGHWUkBrdV2nacD3Nz20qCa0ZXdv56VCvlIkz +4vsnN+Yiuo6OPsPrukRncnrr4NExforwSbL77Cwe0iyVwN5ORKPEk5xVKkn3MBKQWKgq5EUSAElF +E2IryZHeTvABgcK0NR0Cjg+HwSqDWzI1f2QJOXsNjEHDb/KhngocgGrNbtY4MzYtPbTZfruDFgaC +rp4kjdJ5NXZtl75JKu0AMlsAKlsKVv/cuNI+drbHdR+9rwGW9MwRL/3B6IfQMpD6p6eLUpFYtjEf +GH8bWM+z82iV1wk+1LHokC/Ws+AGKwlC415aKNOCXhvbQ6brkgkpDQgL914yLsNxbxxKq+zC7dwa +pI/UsldWO0NlF5urfx9JiduhXtK2Brx0WtRrOIwMr33LQAQju4Y1qbx8EjI+kXiz3jwId2AXWLkt +lcZQA/bFFjDkmKnTOiwYCkcrtpJ59iSlUoHjk6RH52wjOjOINwe90oAnjBvFyWvmyA37sgBwHFei +T4pUg4RS8UaLDrs4MwfblH9yxhyGWYYdBuhKWYzBpWQIPwiGLqxr1EJPOTtPyT7FSpCG048wy1SV +zWgYmbL4kjEJkGEN8BV9aKaogqRxUyOehBC7yZpnu2QZdQ+KPDz6PPh+XecZPA0JtBo6Fck+Sbol +yom122Bk9AgXiJlgiuvyB4pigCIFGELQvqrMA5Ch2womwI15SHpE5ytu5fIRNG9hl+HGf1AZDxsr +zHLQdC0h6oh4vgDXh8/nQacLRCVJFPkzmgCpblBEZYseEaHkFFn0BjpHivbYIv9ImIlWQTG4omB3 +2XR7AplaZU/j63NhUuyc9wdLWqx6SfepwHTuDxSuKGmiUVLh49noZTEDmakQ0MBnGWn74xbpdwsn +E6jngKF1RiE7v5mpwXV2E1n8ZuEaEZEBHmlJ1D/LJyaulW2NFg6k1QROR9dp1SZQhWTlDuybnAnF +uAPghCTk8K8bbn6MY9jYxoGkp0Ayw4tf6sEjPfyOwEDkQ7SvP3pO9J9tvqjR8SwMBYFyP1ODAEkD +QiRK8jSFwxgtkR/gXdjwZcdMq0bgyR34Kc1XCB502cKncpCnE2CNfVTP70ms/RASFT5rofGfLCoq +rlRDAx0t+uC7xEBHeryhx+2UvnO0m/uBvlu51Sh/t0JFjiN8rN5n3gEJQI7zpDhzHFAhR4SnMAnP +8S0zw4UslkJo3ToIlXjAdY5DZEsk+Mxj3ppCgvvtILpWflFmUYVhlMP8OibYkw3EKyI0hNdkQelk +duzyBru3IC82VLIm+CFDb0VGEruJQh4IyYblZC+ckvH+ztE924o0+PqzzgIjly7Q9M3AKdfBLydP +RpunLnrqD48o+zM0faXtKdwcrR1LEuYOt8R7aliLQrWYsT1A63s7XtCh6ywFZIFtVOhqI+A8xMaL +Wb7EJQmJaR+mMAZQ7qTa+zUCcTiX9OaOIQSyEoUtxvK3Z0egkgdyFTRFPh5TZ8yK0uuA9QWSWk6m +fFhXRlROW7o8Hxmq6QtE4CVHiYfq6S6gwkS90gBvnwTVsCqMDn+R1r80TGWJtqlcNJD1KtAXCQ// +LqwAE5mptgt28F9xSLp++841EO6TSeYrsykA+d6nBAGOFLPa3+hZfj6E+t0q/Kw09rcxjCH1Cu+O +FrnWBlgQWem8iW70MjX7GFrI+EaPMc0tuuI3FoQPjX4GZ5Bfp8U3UJGFdV6EXs4+Tw== + + + uvP4yBXo0j25QJPwb5+Pzh6f8fOH0Wodi9ATsgv5XNZQa66AbTT1a7GFPN+zCvNIuBmPq1vtj6Mj +fjueOWcMhZ8I0AmiB3pmzCK3WXIzTr/qA5EyIJuQDj0hUAnfmqfoCUVGMS7muMg9L8GaT2NjYnqh +jeGDcuSfLzEM5P67Vfz/FPVYQ0o+mnC9B9YCWtcVX8rZGUCPgiGX0RwB4UqsOqcw8GKaYj4+cj1w +FOIwBHcFhe1Ak5tKeCxq2KM0BCi7Qa3ut4rrKcviKKirmjBfYQ9/eEnxnbrJpM+Ien5WoZIFgP8j ++410HTwBDjtSSkSz0LpYrVxJ5p+Eufx7G6vxsZp1wm21shXK6cRxCnLfWbYCpD6uM9miaUAYAaSq +nZAvJmbhgTI1ioIVwVo5kyXVSpkceZchOjb4I6Jy4ETW2w+vYpqBSa5T6JBnJV/i5kTloJOXT0eO +zzBg3UkNHuRBDfK3NKwD + + + diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg deleted file mode 100644 index 1c8b4ccc..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json index 32adfd39..52f90f68 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "dinersClub.svg", + "filename" : "DCI_Horizontal-2_ondark.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg new file mode 100644 index 00000000..98d7569b --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg deleted file mode 100644 index 083ed5af..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg deleted file mode 100644 index 06f4891e..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json deleted file mode 100644 index 9b2b69ae..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "placeholder.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg deleted file mode 100644 index 842da557..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/Contents.json similarity index 80% rename from VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/Contents.json rename to VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/Contents.json index 24a71037..72aa17d1 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "credit-card.svg", + "filename" : "UnionPay-logo-onDark.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg new file mode 100644 index 00000000..56a83382 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/Contents.json similarity index 80% rename from VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/Contents.json rename to VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/Contents.json index 5b516479..3bf829c6 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "placeholder-inverted.svg", + "filename" : "UnionPay-logo-onLight.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg new file mode 100644 index 00000000..47bdbd1b --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg @@ -0,0 +1,6 @@ + + + + + + From 4519c7af15527e95038ab385f8d7c95847a02231 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 11:39:09 -0500 Subject: [PATCH 37/87] updated cc cardType logic Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 42441574..aac95a6a 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -52,26 +52,33 @@ extension InputField { } } - static func from(iin: Int) -> CreditCardType? { - switch iin { - case 4000...4999: + static func from(cardNumber: String) -> CreditCardType { + let clean = cardNumber.filter { $0.isNumber } + let firstNum = Int(clean.prefix(1)) ?? 0 + let twoChar = Int(clean.prefix(2)) ?? 0 + let threeChar = Int(clean.prefix(3)) ?? 0 + let fourChar = Int(clean.prefix(4)) ?? 0 + let sixChar = Int(clean.prefix(6)) ?? 0 + + if firstNum == 4 { return .visa - case 5100...5599, 2221...2720: - return .mastercard - case 3400...3499, 3700...3799: + } else if twoChar == 34 || twoChar == 37 { return .amex - case 6011, 6221...6229, 6440...6499, 6500...6599: - return .discover - case 3600...3699, 3800...3999: - return .dinersClub - case 3528...3589: - return .jcb - case 6200...6299, 6000...6010, 8100...8199: + } else if (twoChar == 62 && !(sixChar >= 622126 && sixChar <= 622925)) || twoChar == 81 || (twoChar == 60 && fourChar != 6011) { return .unionPay - default: - return nil + } else if (threeChar > 299 && threeChar <= 305) || threeChar == 309 || twoChar == 36 || twoChar == 38 || twoChar == 39 { + return .dinersClub + } else if fourChar == 6011 || (sixChar >= 622126 && sixChar <= 622925) || (threeChar >= 644 && threeChar <= 649) || twoChar == 65 { + return .discover + } else if fourChar >= 3528 && fourChar <= 3589 { + return .jcb + } else if (twoChar >= 51 && twoChar <= 55) || (sixChar >= 222100 && sixChar <= 272099) { + return .mastercard + } else { + return .generic } } + } class CreditCardHandler: FieldTypeHandler { @@ -171,13 +178,13 @@ extension InputField { defer { inputField.setNeedsUpdate() } guard rawNumber.count >= 4, - let firstFourDigits = Int(String(rawNumber.prefix(4))), - let creditCardType = CreditCardType.from(iin: firstFourDigits) else { + let firstFourDigits = Int(String(rawNumber.prefix(4))) + else { inputField.cardType = .generic return } - inputField.cardType = creditCardType + inputField.cardType = CreditCardType.from(cardNumber: rawNumber) } internal func maskCreditCardNumber(_ cardType: CreditCardType, number: String) -> String { From d4fcb5436b31e0be543d17c596b4b0b3fdee7494 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 13:53:14 -0500 Subject: [PATCH 38/87] removed old code Signed-off-by: Matt Bruce --- .../TextFields/InputField/FieldTypes/CreditCard.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index aac95a6a..0b8c89b6 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -177,9 +177,7 @@ extension InputField { internal func updateCardTypeIcon(_ inputField: InputField, rawNumber: String) { defer { inputField.setNeedsUpdate() } - guard rawNumber.count >= 4, - let firstFourDigits = Int(String(rawNumber.prefix(4))) - else { + guard rawNumber.count >= 4 else { inputField.cardType = .generic return } From 0ab83fa7a83904ad238ba52612298828eda7fe5d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 13:53:26 -0500 Subject: [PATCH 39/87] updated onclick Signed-off-by: Matt Bruce --- .../TextFields/InputField/FieldTypes/InlineAction.swift | 3 ++- VDS/Components/TextFields/InputField/TextLinkModel.swift | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift index 7bacecc0..68fe8b04 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift @@ -19,7 +19,8 @@ extension InputField { override func updateView(_ inputField: InputField) { minWidth = 102.0 - + actionModel = inputField.actionTextLinkModel + super.updateView(inputField) } } diff --git a/VDS/Components/TextFields/InputField/TextLinkModel.swift b/VDS/Components/TextFields/InputField/TextLinkModel.swift index 89235e5e..324ea6a0 100644 --- a/VDS/Components/TextFields/InputField/TextLinkModel.swift +++ b/VDS/Components/TextFields/InputField/TextLinkModel.swift @@ -12,11 +12,11 @@ extension InputField { ///Text that goes in the Tab public var text: String - + ///Click event when you click on a tab - public var onClick: ((TextLink) -> Void)? - - public init(text: String, onClick: ((TextLink) -> Void)? = nil) { + public var onClick: ((String?) -> Void) + + public init(text: String = "Apply", onClick: @escaping (String?) -> Void) { self.text = text self.onClick = onClick } From 3c508aff0e6f57abf843be20b6d6fecb7d47662a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 13:53:36 -0500 Subject: [PATCH 40/87] updated onclick Signed-off-by: Matt Bruce --- .../TextFields/InputField/FieldTypes/FieldType.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index ed5f7e30..a69192f7 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -40,7 +40,7 @@ extension InputField { var keyboardType: UIKeyboardType var minWidth: CGFloat = 40.0 var leftImageName: String? - var actionModel: InputField.TextLinkModel? + var actionModel: TextLinkModel? var toolTipModel: Tooltip.TooltipModel? var isSecureTextEntry = false var placeholderText: String? @@ -66,7 +66,10 @@ extension InputField { inputField.actionTextLink.surface = inputField.surface if let actionModel { inputField.actionTextLink.text = actionModel.text - inputField.actionTextLink.onClick = actionModel.onClick + inputField.actionTextLink.onClick = { [weak self] _ in + guard let self else { return } + actionModel.onClick(inputField.value) + } inputField.actionTextLink.isHidden = false inputField.containerStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) } else { From afea918aa44fdbfcc76e37a1e5624db532c67251 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 14:33:11 -0500 Subject: [PATCH 41/87] fixed issue with error focused Signed-off-by: Matt Bruce --- VDS/Components/TextFields/InputField/InputField.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 6307898f..e4985bee 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -153,8 +153,9 @@ open class InputField: EntryFieldBase { var state = super.state if showSuccess { state.insert(.success) - - } else if textField.isFirstResponder { + } + + if textField.isFirstResponder { state.insert(.focused) } @@ -207,6 +208,7 @@ open class InputField: EntryFieldBase { successLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) + borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) } @@ -332,6 +334,7 @@ open class InputField: EntryFieldBase { extension InputField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { fieldType.handler().textFieldDidBeginEditing(self, textField: textField) + setNeedsUpdate() } public func textFieldDidEndEditing(_ textField: UITextField) { From 84a03573abb2757daf47edc5a01f8b4c14a2dad8 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 14:33:27 -0500 Subject: [PATCH 42/87] updated linkmodel Signed-off-by: Matt Bruce --- .../TextFields/InputField/FieldTypes/FieldType.swift | 9 ++++++--- VDS/Components/TextFields/InputField/TextLinkModel.swift | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index a69192f7..a52dcfae 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -68,7 +68,7 @@ extension InputField { inputField.actionTextLink.text = actionModel.text inputField.actionTextLink.onClick = { [weak self] _ in guard let self else { return } - actionModel.onClick(inputField.value) + actionModel.onClick(inputField) } inputField.actionTextLink.isHidden = false inputField.containerStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) @@ -91,8 +91,11 @@ extension InputField { //placeholder inputField.textField.placeholder = placeholderText - //tooltip - inputField.tooltipModel = toolTipModel + //tooltip from Types take precedence + //if one was set, it would show as usual. + if let toolTipModel { + inputField.tooltipModel = toolTipModel + } } func appendRules(_ inputField: InputField) {} diff --git a/VDS/Components/TextFields/InputField/TextLinkModel.swift b/VDS/Components/TextFields/InputField/TextLinkModel.swift index 324ea6a0..a9c557a0 100644 --- a/VDS/Components/TextFields/InputField/TextLinkModel.swift +++ b/VDS/Components/TextFields/InputField/TextLinkModel.swift @@ -14,9 +14,9 @@ extension InputField { public var text: String ///Click event when you click on a tab - public var onClick: ((String?) -> Void) + public var onClick: ((InputField) -> Void) - public init(text: String = "Apply", onClick: @escaping (String?) -> Void) { + public init(text: String = "Apply", onClick: @escaping (InputField) -> Void) { self.text = text self.onClick = onClick } From 7ec0192945d97f1ede27fc9a5559123e52c9e53d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 16:33:09 -0500 Subject: [PATCH 43/87] added validateOnChange Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 12 ++++++++++-- .../InputField/FieldTypes/FieldType.swift | 1 + .../InputField/FieldTypes/SecurityCode.swift | 17 ++++++++++++++++- .../TextFields/InputField/InputField.swift | 6 +++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 0b8c89b6..7137d895 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - public enum CreditCardType: CaseIterable { + public enum CreditCardType: String, CaseIterable { case generic case visa case mastercard @@ -45,6 +45,14 @@ extension InputField { } } + var securityCodeLength: Int { + if self == .amex { + return 4 + } else { + return 3 + } + } + var maxLength: Int { switch self { case .dinersClub: return 14 @@ -86,13 +94,13 @@ extension InputField { private override init() { super.init() + self.validateOnChange = false self.keyboardType = .numberPad } override func updateView(_ inputField: InputField) { minWidth = 288.0 leftImageName = inputField.cardType.imageName(surface: inputField.surface) - super.updateView(inputField) } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index a52dcfae..039275b2 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -45,6 +45,7 @@ extension InputField { var isSecureTextEntry = false var placeholderText: String? var value: String? + var validateOnChange = false internal override init() { keyboardType = .default diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift index f49800f4..fc219044 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -18,10 +18,25 @@ extension InputField { self.keyboardType = .numberPad } + override func appendRules(_ inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.cardType.securityCodeLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid security code." + } + inputField.rules.append(.init(rule)) + } + } + override func updateView(_ inputField: InputField) { minWidth = 88.0 isSecureTextEntry = true + let title: String = inputField.cardType.rawValue + let content: String = "looking for \(inputField.cardType.securityCodeLength) digits" + toolTipModel = .init(title: title, content: content) + super.updateView(inputField) } @@ -29,7 +44,7 @@ extension InputField { // Allow only numbers and limit the length of text. let allowedCharacters = CharacterSet.decimalDigits let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= 4 + return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= inputField.cardType.securityCodeLength } } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index e4985bee..a01cbc79 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -196,8 +196,12 @@ open class InputField: EntryFieldBase { textField .textPublisher .sink { [weak self] newText in + guard let self else { return } print("textPublisher newText: \(newText)") - self?.sendActions(for: .valueChanged) + if self.fieldType.handler().validateOnChange { + self.validate() + } + self.sendActions(for: .valueChanged) }.store(in: &subscribers) stackView.addArrangedSubview(successLabel) From 20f5926bbf6ad810f605adb4e54998a75a111bdf Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 10 May 2024 13:18:55 +0530 Subject: [PATCH 44/87] Digital ACT-191 ONEAPP-7958 story: callback when the date changes and is in enabled state only. --- VDS/Components/Calendar/Calendar.swift | 27 ++++++++++++------- .../Calendar/CalendarDateViewCell.swift | 5 ++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index fa843db0..bcba3885 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -287,17 +287,24 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let selectedItem = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! - onChangeSelectedDate?(selectedItem) - - selectedDate = self.dates[indexPath.row] - displayDate = selectedDate - var reloadIndexPaths = [indexPath] + // reload selected index, if it is in enabled state. + if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { + let isEnabled: Bool = cell.isDateEnabled() + if isEnabled { + // Callback to pass selected date if it is enabled only. + let selectedItem = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! + onChangeSelectedDate?(selectedItem) + + selectedDate = self.dates[indexPath.row] + displayDate = selectedDate + var reloadIndexPaths = [indexPath] - // If an cell is already selected, then it needs to be deselected. - // Add its index path to the array of index paths to be reloaded. - if let deselectIndexPath = selectedIndexPath { reloadIndexPaths.append(deselectIndexPath) } - self.collectionView.reloadItems(at: reloadIndexPaths) + // If an cell is already selected, then it needs to be deselected. + // Add its index path to the array of index paths to be reloaded. + if let deselectIndexPath = selectedIndexPath { reloadIndexPaths.append(deselectIndexPath) } + self.collectionView.reloadItems(at: reloadIndexPaths) + } + } } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { diff --git a/VDS/Components/Calendar/CalendarDateViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift index 7bbe3fd0..26886d0a 100644 --- a/VDS/Components/Calendar/CalendarDateViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -147,6 +147,11 @@ final class CalendarDateViewCell: UICollectionViewCell { } } + // returns cell enabled state. + func isDateEnabled() -> Bool { + return numberLabel.isEnabled + } + func disableLabel(with surface: Surface) { numberLabel.isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) From 501dc4a55fd7a81cfab3e8f44ec67fbb97e552da Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 09:00:55 -0500 Subject: [PATCH 45/87] refactored the layout for the entryfield Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 4 +-- .../TextFields/EntryFieldBase.swift | 30 +++++++++++-------- .../TextFields/InputField/InputField.swift | 2 +- .../TextFields/TextArea/TextArea.swift | 27 ++++++++--------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 80f94988..d69475d3 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -109,7 +109,7 @@ open class DropdownSelect: EntryFieldBase { $0.axis = .horizontal $0.spacing = VDSFormControls.spaceInset } - controlContainerView.addSubview(controlStackView) + fieldContainerView.addSubview(controlStackView) controlStackView.pinToSuperView() controlStackView.addArrangedSubview(dropdownField) @@ -249,7 +249,7 @@ open class DropdownSelect: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() - var selectedOption = selectedOptionLabel.text ?? "" + let selectedOption = selectedOptionLabel.text ?? "" containerStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")" containerStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 5c549564..94fcd694 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -40,6 +40,19 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + ///This is the stackView that is the first subView of the EntryFieldBase, that will + ///layout the following hierarchy + /// + ///- primaryStackView (vertical) + ///---- titleLabel + ///---- containerView (has the border for the user to interact with. + ///------ containerHorizontal + ///--------- fieldContainerView (subclass implements, ie UITextField, UITextView, etc...) + ///--------- statusIcon (error, succes, or overridden by dev) + ///---- UIView (getBottomContainer) + ///------ bottomContainerStackView (vertical) + ///------- errorLabel + ///--------helpLabel internal var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -63,13 +76,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() - internal var controlContainerView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - }() - - internal var bottomContainerView: UIView = { + internal var fieldContainerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } @@ -229,18 +236,15 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { containerStackView.pinToSuperView(.uniform(VDSLayout.space3X)) //add the view to add input fields - containerStackView.addArrangedSubview(controlContainerView) + containerStackView.addArrangedSubview(fieldContainerView) containerStackView.addArrangedSubview(statusIcon) - containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView) + containerStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) //get the container this is what show helper text, error text //can include other for character count, max length let bottomContainer = getBottomContainer() - //add bottomContainerStackView //this is the vertical stack that contains error text, helper text - bottomContainerView.addSubview(bottomContainerStackView) - bottomContainerStackView.pinToSuperView() bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) @@ -324,7 +328,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { /// Container for the area in which helper or error text presents. open func getBottomContainer() -> UIView { - return bottomContainerView + return bottomContainerStackView } internal func updateRules() { diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index a01cbc79..c416aba7 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -185,7 +185,7 @@ open class InputField: EntryFieldBase { $0.axis = .horizontal $0.spacing = VDSLayout.space3X } - controlContainerView.addSubview(controlStackView) + fieldContainerView.addSubview(controlStackView) controlStackView.pinToSuperView() controlStackView.addArrangedSubview(leftImageView) diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 5ea944a3..8c4a0b15 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -43,17 +43,7 @@ open class TextArea: EntryFieldBase { $0.spacing = VDSLayout.space3X } }() - - internal var bottomStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.distribution = .fill - $0.alignment = .top - $0.spacing = VDSLayout.space2X - } - }() - + open var characterCounterLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall @@ -164,7 +154,7 @@ open class TextArea: EntryFieldBase { containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) minWidthConstraint?.isActive = true - controlContainerView.addSubview(textView) + fieldContainerView.addSubview(textView) textView .pinTop() .pinLeading() @@ -254,9 +244,16 @@ open class TextArea: EntryFieldBase { /// Container for the area showing helper text, error text, character count, maximum length value. open override func getBottomContainer() -> UIView { - bottomStackView.addArrangedSubview(bottomContainerView) - bottomStackView.addArrangedSubview(characterCounterLabel) - return bottomStackView + let stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .top + $0.spacing = VDSLayout.space2X + } + stackView.addArrangedSubview(super.getBottomContainer()) + stackView.addArrangedSubview(characterCounterLabel) + return stackView } /// Used to update any Accessibility properties. From c5262246f32f21a745a0fbb9b3956434bad22117 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 09:10:35 -0500 Subject: [PATCH 46/87] refactred getContainer() to getFieldContainer() to make more sense that the developer needs to only return the Item the User see to interact with. Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 45 +++++++++---------- .../TextFields/EntryFieldBase.swift | 35 ++++++--------- .../InputField/FieldTypes/FieldType.swift | 4 +- .../TextFields/InputField/InputField.swift | 34 ++++++-------- .../TextFields/TextArea/TextArea.swift | 20 +++------ 5 files changed, 58 insertions(+), 80 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index d69475d3..7d938990 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -103,26 +103,10 @@ open class DropdownSelect: EntryFieldBase { open override func setup() { super.setup() - // stackview for controls in EntryFieldBase.controlContainerView - let controlStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.spacing = VDSFormControls.spaceInset - } - fieldContainerView.addSubview(controlStackView) - controlStackView.pinToSuperView() - - controlStackView.addArrangedSubview(dropdownField) - controlStackView.addArrangedSubview(inlineDisplayLabel) - controlStackView.addArrangedSubview(selectedOptionLabel) - - containerStackView.isAccessibilityElement = true - containerStackView.accessibilityLabel = "Dropdown Select" + fieldStackView.isAccessibilityElement = true + fieldStackView.accessibilityLabel = "Dropdown Select" inlineDisplayLabel.isAccessibilityElement = true - controlStackView.setCustomSpacing(0, after: dropdownField) - controlStackView.setCustomSpacing(VDSLayout.space1X, after: inlineDisplayLabel) - controlStackView.setCustomSpacing(VDSLayout.space3X, after: selectedOptionLabel) dropdownField.width(0) inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) inlineWidthConstraint?.isActive = true @@ -150,7 +134,7 @@ open class DropdownSelect: EntryFieldBase { }() // tap gesture - containerStackView + fieldStackView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in self?.launchPicker() @@ -158,6 +142,21 @@ open class DropdownSelect: EntryFieldBase { .store(in: &subscribers) } + open override func getFieldContainer() -> UIView { + let controlStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.spacing = VDSFormControls.spaceInset + } + controlStackView.addArrangedSubview(dropdownField) + controlStackView.addArrangedSubview(inlineDisplayLabel) + controlStackView.addArrangedSubview(selectedOptionLabel) + controlStackView.setCustomSpacing(0, after: dropdownField) + controlStackView.setCustomSpacing(VDSLayout.space1X, after: inlineDisplayLabel) + controlStackView.setCustomSpacing(VDSLayout.space3X, after: selectedOptionLabel) + return controlStackView + } + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() @@ -250,14 +249,14 @@ open class DropdownSelect: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() let selectedOption = selectedOptionLabel.text ?? "" - containerStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")" - containerStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." + fieldStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")" + fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." } open override var accessibilityElements: [Any]? { get { var elements = [Any]() - elements.append(contentsOf: [titleLabel, containerStackView]) + elements.append(contentsOf: [titleLabel, fieldStackView]) if showError { elements.append(statusIcon) @@ -281,7 +280,7 @@ open class DropdownSelect: EntryFieldBase { optionsPicker.isHidden = true dropdownField.resignFirstResponder() setNeedsUpdate() - UIAccessibility.post(notification: .layoutChanged, argument: containerStackView) + UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView) } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 94fcd694..64593441 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -67,7 +67,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() - internal var containerStackView: UIStackView = { + internal var fieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal @@ -76,12 +76,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() - internal var fieldContainerView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - }() - internal var bottomContainerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -224,21 +218,20 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) widthConstraint?.priority = .defaultHigh - - //get the container this is what is color - //border, internal, etc... - let container = getContainer() - + //add ContainerStackView //this is the horizontal stack that contains //the left, InputContainer, Icons, Buttons - container.addSubview(containerStackView) - containerStackView.pinToSuperView(.uniform(VDSLayout.space3X)) + containerView.addSubview(fieldStackView) + fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) + let fieldContainerView = getFieldContainer() + fieldContainerView.translatesAutoresizingMaskIntoConstraints = false + //add the view to add input fields - containerStackView.addArrangedSubview(fieldContainerView) - containerStackView.addArrangedSubview(statusIcon) - containerStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) + fieldStackView.addArrangedSubview(fieldContainerView) + fieldStackView.addArrangedSubview(statusIcon) + fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) //get the container this is what show helper text, error text //can include other for character count, max length @@ -249,11 +242,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { bottomContainerStackView.addArrangedSubview(helperLabel) stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(container) + stackView.addArrangedSubview(containerView) stackView.addArrangedSubview(bottomContainer) stackView.setCustomSpacing(4, after: titleLabel) - stackView.setCustomSpacing(8, after: container) + stackView.setCustomSpacing(8, after: containerView) stackView.setCustomSpacing(8, after: bottomContainer) stackView @@ -322,8 +315,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { // MARK: - Public Methods //-------------------------------------------------- /// Container for the area in which the user interacts. - open func getContainer() -> UIView { - return containerView + open func getFieldContainer() -> UIView { + fatalError("Subclass must return the view that contains the field/view the user will interact with.") } /// Container for the area in which helper or error text presents. diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index 039275b2..2c99b9c5 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -72,10 +72,10 @@ extension InputField { actionModel.onClick(inputField) } inputField.actionTextLink.isHidden = false - inputField.containerStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) + inputField.fieldStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) } else { inputField.actionTextLink.isHidden = true - inputField.containerStackView.setCustomSpacing(0, after: inputField.statusIcon) + inputField.fieldStackView.setCustomSpacing(0, after: inputField.statusIcon) } //set the width constraints diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index c416aba7..327e46d6 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -178,19 +178,7 @@ open class InputField: EntryFieldBase { minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) minWidthConstraint?.isActive = true - - // stackview for controls in EntryFieldBase.controlContainerView - let controlStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.spacing = VDSLayout.space3X - } - fieldContainerView.addSubview(controlStackView) - controlStackView.pinToSuperView() - - controlStackView.addArrangedSubview(leftImageView) - controlStackView.addArrangedSubview(textField) - + textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self textField @@ -207,7 +195,7 @@ open class InputField: EntryFieldBase { stackView.addArrangedSubview(successLabel) stackView.setCustomSpacing(8, after: successLabel) - containerStackView.addArrangedSubview(actionTextLink) + fieldStackView.addArrangedSubview(actionTextLink) successLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() @@ -217,6 +205,18 @@ open class InputField: EntryFieldBase { } + open override func getFieldContainer() -> UIView { + // stackview for controls in EntryFieldBase.controlContainerView + let stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.spacing = VDSLayout.space3X + } + stackView.addArrangedSubview(leftImageView) + stackView.addArrangedSubview(textField) + return stackView + } + /// Resets to default settings. open override func reset() { super.reset() @@ -230,12 +230,6 @@ open class InputField: EntryFieldBase { successText = nil helperTextPlacement = .bottom } - - /// Container for the area in which the user interacts. - open override func getContainer() -> UIView { - inputFieldStackView.addArrangedSubview(containerView) - return inputFieldStackView - } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 8c4a0b15..818ae418 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -151,15 +151,9 @@ open class TextArea: EntryFieldBase { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() - containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) + fieldStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) minWidthConstraint?.isActive = true - fieldContainerView.addSubview(textView) - textView - .pinTop() - .pinLeading() - .pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh) - .pinBottom(0, .defaultHigh) textView.isScrollEnabled = true textView.autocorrectionType = .no @@ -200,13 +194,7 @@ open class TextArea: EntryFieldBase { characterCounterLabel.textStyle = .bodySmall setNeedsUpdate() } - - /// Container for the area in which the user interacts. - open override func getContainer() -> UIView { - inputFieldStackView.addArrangedSubview(containerView) - return inputFieldStackView - } - + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() @@ -242,6 +230,10 @@ open class TextArea: EntryFieldBase { rules.append(.init(countRule)) } + open override func getFieldContainer() -> UIView { + textView + } + /// Container for the area showing helper text, error text, character count, maximum length value. open override func getBottomContainer() -> UIView { let stackView = UIStackView().with { From d870d4f385824d3059d7b548076da0acd81ac0d9 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 09:31:18 -0500 Subject: [PATCH 47/87] refactored dropdown/inputfield for the helper text position Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 23 +++++++++++++++++++ .../TextFields/EntryFieldBase.swift | 18 ++++++++++++--- .../TextFields/InputField/InputField.swift | 21 +++++------------ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 7d938990..bb4d2d97 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -83,6 +83,9 @@ open class DropdownSelect: EntryFieldBase { $0.font = TextStyle.bodyLarge.font } + /// Determines the placement of the helper text. + open var helperTextPlacement: HelperTextPlacement = .bottom { didSet { setNeedsUpdate() } } + open var optionsPicker = UIPickerView() //-------------------------------------------------- @@ -246,6 +249,26 @@ open class DropdownSelect: EntryFieldBase { statusIcon.color = iconColorConfiguration.getColor(self) } + open override func updateHelperLabel(){ + //remove first + helperLabel.removeFromSuperview() + + super.updateHelperLabel() + + //set the helper label position + if helperText != nil { + if helperTextPlacement == .right { + middleStackView.spacing = 12 + middleStackView.distribution = .fillEqually + middleStackView.addArrangedSubview(helperLabel) + } else { + middleStackView.spacing = 0 + middleStackView.distribution = .fill + bottomContainerStackView.addArrangedSubview(helperLabel) + } + } + } + open override func updateAccessibility() { super.updateAccessibility() let selectedOption = selectedOptionLabel.text ?? "" diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 64593441..ea833187 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -60,7 +60,16 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { $0.distribution = .fill } }() - + + internal var middleStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .top + } + }() + internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -219,6 +228,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) widthConstraint?.priority = .defaultHigh + //add the containerView to the middleStack + middleStackView.addArrangedSubview(containerView) + //add ContainerStackView //this is the horizontal stack that contains //the left, InputContainer, Icons, Buttons @@ -242,11 +254,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { bottomContainerStackView.addArrangedSubview(helperLabel) stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(containerView) + stackView.addArrangedSubview(middleStackView) stackView.addArrangedSubview(bottomContainer) stackView.setCustomSpacing(4, after: titleLabel) - stackView.setCustomSpacing(8, after: containerView) + stackView.setCustomSpacing(8, after: middleStackView) stackView.setCustomSpacing(8, after: bottomContainer) stackView diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 327e46d6..49f0cfbc 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -34,15 +34,6 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var inputFieldStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.distribution = .fill - $0.spacing = 12 - } - }() - internal var minWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- @@ -274,13 +265,13 @@ open class InputField: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - inputFieldStackView.spacing = 12 - inputFieldStackView.distribution = .fillEqually - inputFieldStackView.addArrangedSubview(helperLabel) + middleStackView.spacing = 12 + middleStackView.distribution = .fillEqually + middleStackView.addArrangedSubview(helperLabel) } else { - inputFieldStackView.spacing = 0 - inputFieldStackView.distribution = .fill - stackView.addArrangedSubview(helperLabel) + middleStackView.spacing = 0 + middleStackView.distribution = .fill + bottomContainerStackView.addArrangedSubview(helperLabel) } } } From 0af79c97b7423979a3a214e706643f4890731366 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 10:08:10 -0500 Subject: [PATCH 48/87] refactored to helpertext to the right Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 12 ++++++++++++ VDS/Components/TextFields/EntryFieldBase.swift | 7 ++++--- .../TextFields/InputField/InputField.swift | 15 +++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index bb4d2d97..6476819d 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -92,6 +92,7 @@ open class DropdownSelect: EntryFieldBase { // MARK: - Constraints //-------------------------------------------------- internal var inlineWidthConstraint: NSLayoutConstraint? + internal var titleLabelWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration Properties @@ -106,6 +107,10 @@ open class DropdownSelect: EntryFieldBase { open override func setup() { super.setup() + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + titleLabel.setContentHuggingPriority(.required, for: .horizontal) + titleLabelWidthConstraint = titleLabel.width(constant: 0) + fieldStackView.isAccessibilityElement = true fieldStackView.accessibilityLabel = "Dropdown Select" inlineDisplayLabel.isAccessibilityElement = true @@ -305,6 +310,13 @@ open class DropdownSelect: EntryFieldBase { setNeedsUpdate() UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView) } + + open override func layoutSubviews() { + super.layoutSubviews() + titleLabelWidthConstraint?.constant = containerView.frame.width + titleLabelWidthConstraint?.isActive = helperTextPlacement == .right + } + } //-------------------------------------------------- diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index ea833187..0581c4bb 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -58,6 +58,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill + $0.alignment = .leading } }() @@ -209,7 +210,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -227,10 +228,10 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) widthConstraint?.priority = .defaultHigh - + //add the containerView to the middleStack middleStackView.addArrangedSubview(containerView) - + //add ContainerStackView //this is the horizontal stack that contains //the left, InputContainer, Icons, Buttons diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 49f0cfbc..62181d69 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -35,7 +35,8 @@ open class InputField: EntryFieldBase { // MARK: - Private Properties //-------------------------------------------------- internal var minWidthConstraint: NSLayoutConstraint? - + internal var titleLabelWidthConstraint: NSLayoutConstraint? + //-------------------------------------------------- // MARK: - Public FieldType Properties //-------------------------------------------------- @@ -167,6 +168,10 @@ open class InputField: EntryFieldBase { open override func setup() { super.setup() + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + titleLabel.setContentHuggingPriority(.required, for: .horizontal) + titleLabelWidthConstraint = titleLabel.width(constant: 0) + minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) minWidthConstraint?.isActive = true @@ -275,7 +280,7 @@ open class InputField: EntryFieldBase { } } } - + override func updateRules() { super.updateRules() fieldType.handler().appendRules(self) @@ -310,6 +315,12 @@ open class InputField: EntryFieldBase { set { super.accessibilityElements = newValue } } + open override func layoutSubviews() { + super.layoutSubviews() + titleLabelWidthConstraint?.constant = containerView.frame.width + titleLabelWidthConstraint?.isActive = helperTextPlacement == .right + } + open override var canBecomeFirstResponder: Bool { true } open override func resignFirstResponder() -> Bool { From a7d777a97d2c9eb621eb7b8e5a29bed7332e4d8a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 10:30:37 -0500 Subject: [PATCH 49/87] refactored more for the naming Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 2 +- .../TextFields/EntryFieldBase.swift | 33 ++++++++----------- .../TextFields/InputField/InputField.swift | 2 +- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 6476819d..c401267e 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -263,7 +263,7 @@ open class DropdownSelect: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - middleStackView.spacing = 12 + middleStackView.spacing = VDSLayout.space3X middleStackView.distribution = .fillEqually middleStackView.addArrangedSubview(helperLabel) } else { diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 0581c4bb..44993c39 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -40,19 +40,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - ///This is the stackView that is the first subView of the EntryFieldBase, that will - ///layout the following hierarchy - /// - ///- primaryStackView (vertical) - ///---- titleLabel - ///---- containerView (has the border for the user to interact with. - ///------ containerHorizontal - ///--------- fieldContainerView (subclass implements, ie UITextField, UITextView, etc...) - ///--------- statusIcon (error, succes, or overridden by dev) - ///---- UIView (getBottomContainer) - ///------ bottomContainerStackView (vertical) - ///------- errorLabel - ///--------helpLabel internal var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -91,6 +78,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill + $0.spacing = VDSLayout.space2X } }() @@ -229,9 +217,18 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) widthConstraint?.priority = .defaultHigh + let leftStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.alignment = .leading + } + leftStackView.addArrangedSubview(containerView) + leftStackView.setCustomSpacing(8, after: containerView) + //add the containerView to the middleStack - middleStackView.addArrangedSubview(containerView) - + middleStackView.addArrangedSubview(leftStackView) + //add ContainerStackView //this is the horizontal stack that contains //the left, InputContainer, Icons, Buttons @@ -249,18 +246,16 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //get the container this is what show helper text, error text //can include other for character count, max length let bottomContainer = getBottomContainer() - + //this is the vertical stack that contains error text, helper text bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(middleStackView) - stackView.addArrangedSubview(bottomContainer) + leftStackView.addArrangedSubview(bottomContainer) stackView.setCustomSpacing(4, after: titleLabel) - stackView.setCustomSpacing(8, after: middleStackView) - stackView.setCustomSpacing(8, after: bottomContainer) stackView .pinTop() diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 62181d69..9150ae6d 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -270,7 +270,7 @@ open class InputField: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - middleStackView.spacing = 12 + middleStackView.spacing = VDSLayout.space3X middleStackView.distribution = .fillEqually middleStackView.addArrangedSubview(helperLabel) } else { From c02ba88a761c0549acd3f20089185100509eacb9 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 11:02:44 -0500 Subject: [PATCH 50/87] updated constraint Signed-off-by: Matt Bruce --- VDS/Components/TextFields/EntryFieldBase.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 44993c39..83ca5a40 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -214,14 +214,12 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { heightConstraint?.priority = .defaultHigh heightConstraint?.isActive = true - widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) - widthConstraint?.priority = .defaultHigh - + widthConstraint = containerView.width(constant: 0) + let leftStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill - $0.alignment = .leading } leftStackView.addArrangedSubview(containerView) leftStackView.setCustomSpacing(8, after: containerView) From 30b380fceb26e07ba359d0773d0bbfb95e538b46 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 11:02:54 -0500 Subject: [PATCH 51/87] added width constraint check Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index c401267e..85c2c104 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -64,6 +64,8 @@ open class DropdownSelect: EntryFieldBase { // MARK: - Public Properties //-------------------------------------------------- open var inlineDisplayLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) $0.textAlignment = .left $0.textStyle = .boldBodyLarge $0.lineBreakMode = .byCharWrapping @@ -72,6 +74,7 @@ open class DropdownSelect: EntryFieldBase { open var selectedOptionLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) $0.textAlignment = .left $0.textStyle = .bodyLarge $0.lineBreakMode = .byCharWrapping @@ -91,13 +94,15 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- + internal var maxWidthConstraint: NSLayoutConstraint? + internal var minWidthConstraint: NSLayoutConstraint? internal var inlineWidthConstraint: NSLayoutConstraint? internal var titleLabelWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- - internal override var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) } + internal override var containerSize: CGSize { .init(width: minWidthDefault, height: 44) } //-------------------------------------------------- // MARK: - Overrides @@ -105,7 +110,9 @@ open class DropdownSelect: EntryFieldBase { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { - super.setup() + super.setup() + minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: containerSize.width) + maxWidthConstraint = containerView.width(constant: containerSize.width) titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) @@ -154,6 +161,7 @@ open class DropdownSelect: EntryFieldBase { let controlStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal + $0.distribution = .fill $0.spacing = VDSFormControls.spaceInset } controlStackView.addArrangedSubview(dropdownField) @@ -174,6 +182,20 @@ open class DropdownSelect: EntryFieldBase { dropdownField.isUserInteractionEnabled = isReadOnly ? false : true selectedOptionLabel.surface = surface selectedOptionLabel.isEnabled = isEnabled + + //set the width constraints + if let width { + widthConstraint?.constant = width + widthConstraint?.isActive = true + minWidthConstraint?.isActive = false + maxWidthConstraint?.isActive = false + } else { + minWidthConstraint?.constant = showInlineLabel ? minWidthInlineLabel : minWidthDefault + maxWidthConstraint?.constant = frame.width + widthConstraint?.isActive = false + minWidthConstraint?.isActive = true + maxWidthConstraint?.isActive = true + } } /// Resets to default settings. From 4a337c6fcba557603f912c0acaa941d96282494a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 11:06:59 -0500 Subject: [PATCH 52/87] fixed minwidth bug Signed-off-by: Matt Bruce --- VDS/Components/DropdownSelect/DropdownSelect.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 85c2c104..aab62e19 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -59,7 +59,7 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 - + internal var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -184,13 +184,13 @@ open class DropdownSelect: EntryFieldBase { selectedOptionLabel.isEnabled = isEnabled //set the width constraints - if let width { + if let width, width > minWidth { widthConstraint?.constant = width widthConstraint?.isActive = true minWidthConstraint?.isActive = false maxWidthConstraint?.isActive = false } else { - minWidthConstraint?.constant = showInlineLabel ? minWidthInlineLabel : minWidthDefault + minWidthConstraint?.constant = minWidth maxWidthConstraint?.constant = frame.width widthConstraint?.isActive = false minWidthConstraint?.isActive = true From 843c8054ad10bffc630a62cbc2cf9bbf1aa42f88 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 10 May 2024 22:40:37 +0530 Subject: [PATCH 53/87] Digital ACT-191 ONEAPP-7958 story: dynamic height update for calendar --- VDS/Components/Calendar/Calendar.swift | 28 +++++++++++++------- VDS/Components/Calendar/Date+Extension.swift | 4 ++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index bcba3885..9ec8e6fc 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -74,8 +74,9 @@ open class CalendarBase: View { private let headerHeight = 88.0 private let footerHeight = 40.0 private let calendarWidth = 304.0 - private let items = 35 - + + private var heightConstraint: NSLayoutConstraint? + private var containerHeightConstraint: NSLayoutConstraint? private var selectedIndexPath : IndexPath? private var dates: [Date] = [] private var days: [String] = [] @@ -121,15 +122,16 @@ open class CalendarBase: View { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() - isAccessibilityElement = false + isAccessibilityElement = true + accessibilityLabel = "Calendar" addSubview(containerView) containerView .pinTop() .pinBottom() .pinLeadingGreaterThanOrEqualTo() .pinTrailingLessThanOrEqualTo() - .height(containerSize.height) .width(containerSize.width) + .heightGreaterThanEqualTo(containerSize.height) containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() // Calendar View @@ -142,10 +144,10 @@ open class CalendarBase: View { .pinLeading(spacing) .pinTrailing(spacing) .width(calendarWidth) - .height(calendarHeight) + .heightGreaterThanEqualTo(calendarHeight) collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() } - + open override func updateView() { super.updateView() // range check between min & max dates @@ -154,8 +156,8 @@ open class CalendarBase: View { let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate) displayDate = fallsBetween ? displayDate : minDate self.fetchDates(with: displayDate) - collectionView.reloadData() } + layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor if hideContainerBorder { layer.borderColor = nil @@ -187,6 +189,8 @@ open class CalendarBase: View { // MARK: - Private Methods //-------------------------------------------------- func fetchDates(with aDate:Date) { + heightConstraint?.isActive = false + containerHeightConstraint?.isActive = false days.removeAll() self.dates = aDate.calendarDisplayDays for date in dates { @@ -197,6 +201,14 @@ open class CalendarBase: View { days.append(getDay(with: date)) } } + self.collectionView.reloadData() + var height = self.collectionView.collectionViewLayout.collectionViewContentSize.height + height = (height > 0) ? height : containerSize.height + heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: height) + containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: (height + (2 * VDSLayout.space4X))) + heightConstraint?.isActive = true + containerHeightConstraint?.isActive = true + self.layoutIfNeeded() } func getDay(with date:Date) -> String { @@ -259,7 +271,6 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI if ((aDate.monthInt <= maxDate.monthInt) && (aDate.yearInt == maxDate.yearInt)) || (aDate.yearInt < maxDate.yearInt) { displayDate = aDate self.fetchDates(with: displayDate) - self.collectionView.reloadData() } } header.previousClicked = { [weak self] in @@ -268,7 +279,6 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI if ((minDate.monthInt <= aDate.monthInt) && (minDate.yearInt == aDate.yearInt)) || (minDate.yearInt < aDate.yearInt) { displayDate = aDate self.fetchDates(with: displayDate) - self.collectionView.reloadData() } } header.update(with: surface, aDate: displayDate, nextEnabled: nextEnabled, previousEnabled: prevEnabled) diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 2a016ecb..8d697049 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -92,7 +92,9 @@ extension Date { /// Check if the date falls between the given dates func isBetweeen(date date1: Date, andDate date2: Date) -> Bool { - return date1.compare(self) == self.compare(date2) + let from = Calendar.current.date(byAdding: .day, value: -1, to: date1)! + let to = Calendar.current.date(byAdding: .day, value: 1, to: date2)! + return from.compare(self) == self.compare(to) } /// Returns the month name of the given date From 84314040d90bd9dd4c188840b59557a43ae6e985 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 14:36:50 -0500 Subject: [PATCH 54/87] refactored tooltip for securityCode Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/SecurityCode.swift | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift index fc219044..cf60eb9a 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -7,6 +7,7 @@ import Foundation import UIKit +import VDSTokens extension InputField { @@ -32,14 +33,73 @@ extension InputField { override func updateView(_ inputField: InputField) { minWidth = 88.0 isSecureTextEntry = true - - let title: String = inputField.cardType.rawValue - let content: String = "looking for \(inputField.cardType.securityCodeLength) digits" - toolTipModel = .init(title: title, content: content) - + toolTipModel = getToolTip(inputField) super.updateView(inputField) } + func getToolTip(_ inputField: InputField) -> Tooltip.TooltipModel { + + let surface = inputField.surface + + var contentView: UIView + var code3Label = Label().with { + $0.text = "Most credit or debit cards have a 3-digit security code on the back." + $0.isEnabled = true + $0.surface = surface + } + var code4Label = Label().with { + $0.text = "American Express cards have a 4-digit code on the front." + $0.isEnabled = true + $0.surface = surface + } + + var code3ImageView = UIImageView().with { + $0.image = BundleManager.shared.image(for: "securityCode\(surface == .dark ? "-inverted": "")") + } + var code4ImageView = UIImageView().with { + $0.image = BundleManager.shared.image(for: "securityCodeAmex\(surface == .dark ? "-inverted": "")") + } + + func stack(_ axis: NSLayoutConstraint.Axis = .vertical) -> UIStackView { + UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = axis + $0.distribution = .fill + $0.alignment = .leading + $0.spacing = VDSLayout.space2X + } + } + + func view3() -> UIView { + stack().with { + $0.addArrangedSubview(code3ImageView) + $0.addArrangedSubview(code3Label) + } + } + + func view4() -> UIView { + stack().with { + $0.addArrangedSubview(code4ImageView) + $0.addArrangedSubview(code4Label) + } + } + + let title: String = inputField.cardType.rawValue + switch inputField.cardType { + case .amex: + contentView = view4() + case .generic: + contentView = stack(.horizontal).with { + $0.addArrangedSubview(view3()) + $0.addArrangedSubview(view4()) + } + default: + contentView = view3() + } + + return .init(contentView: contentView) + } + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { // Allow only numbers and limit the length of text. let allowedCharacters = CharacterSet.decimalDigits From 2f6df89f0900dbd9fcdc627009f29d1a278f1752 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 14:48:20 -0500 Subject: [PATCH 55/87] added focused state Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index aab62e19..febd3323 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -31,6 +31,18 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- + /// Override UIControl state to add the .error state if showSuccess is true and if showError is true. + open override var state: UIControl.State { + get { + var state = super.state + if dropdownField.isFirstResponder { + state.insert(.focused) + } + + return state + } + } + /// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input. open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }} @@ -53,7 +65,7 @@ open class DropdownSelect: EntryFieldBase { /// A callback when the selected option changes. Passes parameters (option). open var onItemSelected: ((Int, DropdownOptionModel) -> Void)? - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -338,14 +350,22 @@ open class DropdownSelect: EntryFieldBase { titleLabelWidthConstraint?.constant = containerView.frame.width titleLabelWidthConstraint?.isActive = helperTextPlacement == .right } + + open override var canBecomeFirstResponder: Bool { true } + open override func resignFirstResponder() -> Bool { + if dropdownField.isFirstResponder { + dropdownField.resignFirstResponder() + } + return super.resignFirstResponder() + } } //-------------------------------------------------- // MARK: - UIPickerView Delegate & Datasource //-------------------------------------------------- extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { - + internal func launchPicker() { if optionsPicker.isHidden { UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker) @@ -354,6 +374,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { dropdownField.resignFirstResponder() } optionsPicker.isHidden = !optionsPicker.isHidden + setNeedsUpdate() } public func numberOfComponents(in pickerView: UIPickerView) -> Int { From a927be7517c56963003a7385a85634f94f70c9a0 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 15:10:39 -0500 Subject: [PATCH 56/87] added release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index e7193230..39447d3e 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,3 +1,8 @@ +1.0.63 +---------------- +- CXTDT-555860 - Dropdown Select - Form Elements layout option missing + + 1.0.62 ---------------- - CXTDT-546824 - Notification - Accessibility - Redundant text is provided for the notification icon. From aeb620a8a13b7daa448966b32057ab00d4841cf6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 16:03:21 -0500 Subject: [PATCH 57/87] got width working correctly Signed-off-by: Matt Bruce --- .../TextFields/TextArea/TextArea.swift | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 818ae418..61a87e13 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -32,6 +32,7 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal var maxWidthConstraint: NSLayoutConstraint? internal var minWidthConstraint: NSLayoutConstraint? internal var textViewHeightConstraint: NSLayoutConstraint? @@ -152,9 +153,9 @@ open class TextArea: EntryFieldBase { open override func setup() { super.setup() fieldStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) - minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) - minWidthConstraint?.isActive = true - + minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: containerSize.width) + maxWidthConstraint = containerView.width(constant: containerSize.width) + textView.isScrollEnabled = true textView.autocorrectionType = .no @@ -184,6 +185,11 @@ open class TextArea: EntryFieldBase { borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused) characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() bottomContainerStackView.spacing = VDSLayout.space2X + + widthConstraint?.isActive = false + minWidthConstraint?.isActive = true + maxWidthConstraint?.isActive = true + } /// Resets to default settings. @@ -203,15 +209,8 @@ open class TextArea: EntryFieldBase { textView.surface = surface //set the width constraints - if let width { - widthConstraint?.constant = width - widthConstraint?.isActive = true - minWidthConstraint?.isActive = false - } else { - minWidthConstraint?.constant = containerSize.width - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - } + minWidthConstraint?.constant = containerSize.width + maxWidthConstraint?.constant = frame.width characterCounterLabel.text = getCharacterCounterText() From 3859c8eeb72960cd953424620c51a9ef96d5eea8 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 16:17:12 -0500 Subject: [PATCH 58/87] refactored width to fields Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/FieldType.swift | 15 ++++++++++++- .../TextFields/InputField/InputField.swift | 6 ++--- .../TextFields/TextArea/TextArea.swift | 22 +++++-------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index 2c99b9c5..59906df7 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -78,15 +78,28 @@ extension InputField { inputField.fieldStackView.setCustomSpacing(0, after: inputField.statusIcon) } +// //set the width constraints +// if let width = inputField.width, width > minWidth { +// inputField.widthConstraint?.constant = width +// inputField.widthConstraint?.isActive = true +// inputField.minWidthConstraint?.isActive = false +// } else { +// inputField.minWidthConstraint?.constant = minWidth +// inputField.widthConstraint?.isActive = false +// inputField.minWidthConstraint?.isActive = true +// } //set the width constraints - if let width = inputField.width, width > minWidth { + if let width = inputField.width, width > minWidth { inputField.widthConstraint?.constant = width inputField.widthConstraint?.isActive = true inputField.minWidthConstraint?.isActive = false + inputField.maxWidthConstraint?.isActive = false } else { inputField.minWidthConstraint?.constant = minWidth + inputField.maxWidthConstraint?.constant = inputField.frame.width inputField.widthConstraint?.isActive = false inputField.minWidthConstraint?.isActive = true + inputField.maxWidthConstraint?.isActive = true } //placeholder diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 9150ae6d..847e5da8 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -34,6 +34,7 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal var maxWidthConstraint: NSLayoutConstraint? internal var minWidthConstraint: NSLayoutConstraint? internal var titleLabelWidthConstraint: NSLayoutConstraint? @@ -171,9 +172,8 @@ open class InputField: EntryFieldBase { titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabelWidthConstraint = titleLabel.width(constant: 0) - - minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) - minWidthConstraint?.isActive = true + maxWidthConstraint = containerView.width(constant: containerSize.width) + minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: 0) textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 61a87e13..9879c54d 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -51,21 +51,8 @@ open class TextArea: EntryFieldBase { $0.textAlignment = .right $0.numberOfLines = 1 } - - private var _minHeight: Height = .twoX - - open var minHeight: Height? { - get { return _minHeight } - set { - if let newValue { - _minHeight = newValue - } else { - _minHeight = .twoX - } - textViewHeightConstraint?.constant = _minHeight.value - setNeedsUpdate() - } - } + + open var minHeight: Height = .twoX { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Public Properties @@ -211,7 +198,8 @@ open class TextArea: EntryFieldBase { //set the width constraints minWidthConstraint?.constant = containerSize.width maxWidthConstraint?.constant = frame.width - + textViewHeightConstraint?.constant = minHeight.value + characterCounterLabel.text = getCharacterCounterText() statusIcon.color = iconColorConfiguration.getColor(self) @@ -309,7 +297,7 @@ open class TextArea: EntryFieldBase { //since it will autogrow with the current settings if let textViewHeightConstraint, textView.isEditable { var height = textView.contentSize.height - height = max(height, _minHeight.value) + height = max(height, minHeight.value) if height > Height.twoX.value && height <= Height.fourX.value { textViewHeightConstraint.constant = Height.fourX.value } else if height > Height.fourX.value { From 18090871ee2fb6012c086633495d32428edbd780 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 16:52:28 -0500 Subject: [PATCH 59/87] removed min/max, you only need width + calculations Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 18 ++++-------------- VDS/Components/TextFields/EntryFieldBase.swift | 5 +---- .../TextFields/TextArea/TextArea.swift | 13 +++---------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index febd3323..bf1603a9 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -106,8 +106,6 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- - internal var maxWidthConstraint: NSLayoutConstraint? - internal var minWidthConstraint: NSLayoutConstraint? internal var inlineWidthConstraint: NSLayoutConstraint? internal var titleLabelWidthConstraint: NSLayoutConstraint? @@ -123,9 +121,7 @@ open class DropdownSelect: EntryFieldBase { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() - minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: containerSize.width) - maxWidthConstraint = containerView.width(constant: containerSize.width) - + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabelWidthConstraint = titleLabel.width(constant: 0) @@ -196,17 +192,11 @@ open class DropdownSelect: EntryFieldBase { selectedOptionLabel.isEnabled = isEnabled //set the width constraints - if let width, width > minWidth { + let maxwidth = frame.size.width + if let width, width > minWidth && width < maxwidth { widthConstraint?.constant = width - widthConstraint?.isActive = true - minWidthConstraint?.isActive = false - maxWidthConstraint?.isActive = false } else { - minWidthConstraint?.constant = minWidth - maxWidthConstraint?.constant = frame.width - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - maxWidthConstraint?.isActive = true + widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 83ca5a40..f34a2259 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -210,10 +210,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { addSubview(stackView) //create the wrapping view - heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) - heightConstraint?.priority = .defaultHigh - heightConstraint?.isActive = true - + heightConstraint = containerView.heightGreaterThanEqualTo(constant: containerSize.height) widthConstraint = containerView.width(constant: 0) let leftStackView = UIStackView().with { diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 9879c54d..3913f2e6 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -32,8 +32,6 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var maxWidthConstraint: NSLayoutConstraint? - internal var minWidthConstraint: NSLayoutConstraint? internal var textViewHeightConstraint: NSLayoutConstraint? internal var inputFieldStackView: UIStackView = { @@ -140,8 +138,6 @@ open class TextArea: EntryFieldBase { open override func setup() { super.setup() fieldStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) - minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: containerSize.width) - maxWidthConstraint = containerView.width(constant: containerSize.width) textView.isScrollEnabled = true textView.autocorrectionType = .no @@ -173,10 +169,6 @@ open class TextArea: EntryFieldBase { characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() bottomContainerStackView.spacing = VDSLayout.space2X - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - maxWidthConstraint?.isActive = true - } /// Resets to default settings. @@ -196,8 +188,9 @@ open class TextArea: EntryFieldBase { textView.surface = surface //set the width constraints - minWidthConstraint?.constant = containerSize.width - maxWidthConstraint?.constant = frame.width + let maxwidth = frame.size.width + let minWidth = containerSize.width + widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth textViewHeightConstraint?.constant = minHeight.value characterCounterLabel.text = getCharacterCounterText() From 6ad9156e134d9da10217e266881072e1b9cbbad7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 17:04:03 -0500 Subject: [PATCH 60/87] resolved input field Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/FieldType.swift | 30 +++++-------------- .../TextFields/InputField/InputField.swift | 6 +--- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index 59906df7..de04aa1d 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -54,6 +54,9 @@ extension InputField { func updateView(_ inputField: InputField) { + //keyboard + inputField.textField.keyboardType = keyboardType + //textField inputField.textField.isSecureTextEntry = isSecureTextEntry @@ -67,8 +70,7 @@ extension InputField { inputField.actionTextLink.surface = inputField.surface if let actionModel { inputField.actionTextLink.text = actionModel.text - inputField.actionTextLink.onClick = { [weak self] _ in - guard let self else { return } + inputField.actionTextLink.onClick = { _ in actionModel.onClick(inputField) } inputField.actionTextLink.isHidden = false @@ -78,33 +80,17 @@ extension InputField { inputField.fieldStackView.setCustomSpacing(0, after: inputField.statusIcon) } -// //set the width constraints -// if let width = inputField.width, width > minWidth { -// inputField.widthConstraint?.constant = width -// inputField.widthConstraint?.isActive = true -// inputField.minWidthConstraint?.isActive = false -// } else { -// inputField.minWidthConstraint?.constant = minWidth -// inputField.widthConstraint?.isActive = false -// inputField.minWidthConstraint?.isActive = true -// } //set the width constraints - if let width = inputField.width, width > minWidth { + let maxwidth = inputField.frame.size.width + if let width = inputField.width, width > minWidth && width < maxwidth { inputField.widthConstraint?.constant = width - inputField.widthConstraint?.isActive = true - inputField.minWidthConstraint?.isActive = false - inputField.maxWidthConstraint?.isActive = false } else { - inputField.minWidthConstraint?.constant = minWidth - inputField.maxWidthConstraint?.constant = inputField.frame.width - inputField.widthConstraint?.isActive = false - inputField.minWidthConstraint?.isActive = true - inputField.maxWidthConstraint?.isActive = true + inputField.widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth } //placeholder inputField.textField.placeholder = placeholderText - + //tooltip from Types take precedence //if one was set, it would show as usual. if let toolTipModel { diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 847e5da8..57d6060a 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -34,8 +34,6 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var maxWidthConstraint: NSLayoutConstraint? - internal var minWidthConstraint: NSLayoutConstraint? internal var titleLabelWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- @@ -86,7 +84,7 @@ open class InputField: EntryFieldBase { }.eraseToAnyColorable() open var leftImageView = UIImageView().with { - $0.height(21) + $0.height(20) $0.width(32) $0.isAccessibilityElement = false $0.translatesAutoresizingMaskIntoConstraints = false @@ -172,8 +170,6 @@ open class InputField: EntryFieldBase { titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabelWidthConstraint = titleLabel.width(constant: 0) - maxWidthConstraint = containerView.width(constant: containerSize.width) - minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: 0) textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self From d7fc65612f930c9b8fdc6300add3a5b14966cd74 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 10 May 2024 17:23:04 -0500 Subject: [PATCH 61/87] fixed password link bug Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/FieldType.swift | 8 +++---- .../InputField/FieldTypes/Password.swift | 14 +++++++++++++ .../TextFields/InputField/InputField.swift | 21 +++++++++---------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index de04aa1d..db45d767 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -100,11 +100,11 @@ extension InputField { func appendRules(_ inputField: InputField) {} - func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { - } + func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { } - func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) { - } + func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {} + + func textFieldDidChangeSelection(_ inputField: InputField, textField: UITextField) {} func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { return true diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift index 892a8b9b..00c00a0d 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift @@ -48,6 +48,20 @@ extension InputField { minWidth = 62.0 super.updateView(inputField) + + updateLink(inputField) + } + + func updateLink(_ inputField: InputField) { + if let text = inputField.textField.text, !text.isEmpty { + inputField.actionTextLink.isHidden = false + } else { + inputField.actionTextLink.isHidden = true + } + } + + override func textFieldDidChangeSelection(_ inputField: InputField, textField: UITextField) { + updateLink(inputField) } } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 57d6060a..e11d1253 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -173,17 +173,6 @@ open class InputField: EntryFieldBase { textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self - textField - .textPublisher - .sink { [weak self] newText in - guard let self else { return } - print("textPublisher newText: \(newText)") - if self.fieldType.handler().validateOnChange { - self.validate() - } - self.sendActions(for: .valueChanged) - }.store(in: &subscribers) - stackView.addArrangedSubview(successLabel) stackView.setCustomSpacing(8, after: successLabel) @@ -338,6 +327,16 @@ extension InputField: UITextFieldDelegate { validate() } + public func textFieldDidChangeSelection(_ textField: UITextField) { + fieldType.handler().textFieldDidChangeSelection(self, textField: textField) + if fieldType.handler().validateOnChange { + validate() + } else { + setNeedsUpdate() + } + sendActions(for: .valueChanged) + } + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string) } From 8f529251fd79e0315d716f7469b990fc8055ee78 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 10:11:15 -0500 Subject: [PATCH 62/87] added datePicker Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c60f125a..d04570b2 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C132BED0DEC00BA39FA /* Number.swift */; }; EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C152BED0E0300BA39FA /* InlineAction.swift */; }; EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; + EAC58C212BF127FE00BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C202BF127FE00BA39FA /* DatePicker.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -355,6 +356,7 @@ EAC58C132BED0DEC00BA39FA /* Number.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Number.swift; sourceTree = ""; }; EAC58C152BED0E0300BA39FA /* InlineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAction.swift; sourceTree = ""; }; EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; + EAC58C202BF127FE00BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatePicker.swift; path = ../../../../../../../../../Downloads/DatePicker.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -612,6 +614,7 @@ EA0FC2BE2912D18200DF80B4 /* Buttons */, 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */, EAF7F092289985E200B287F5 /* Checkbox */, + EAC58C1F2BF127F000BA39FA /* DatePicker */, 186D13C92BBA8A3500986B53 /* DropdownSelect */, EA985BF3296C609E00F2FF2E /* Icon */, EA3362412892EF700071C351 /* Label */, @@ -909,6 +912,14 @@ path = FieldTypes; sourceTree = ""; }; + EAC58C1F2BF127F000BA39FA /* DatePicker */ = { + isa = PBXGroup; + children = ( + EAC58C202BF127FE00BA39FA /* DatePicker.swift */, + ); + path = DatePicker; + sourceTree = ""; + }; EAC9257E29119B5D00091998 /* TextLink */ = { isa = PBXGroup; children = ( @@ -1157,6 +1168,7 @@ 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, + EAC58C212BF127FE00BA39FA /* DatePicker.swift in Sources */, EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */, 71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, From e7e6a68458e04a8e83d7b148fa364152e8036b11 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 10:11:25 -0500 Subject: [PATCH 63/87] added calendar Signed-off-by: Matt Bruce --- VDS/Components/Icon/IconName.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/Components/Icon/IconName.swift b/VDS/Components/Icon/IconName.swift index 3a17f686..27f57787 100644 --- a/VDS/Components/Icon/IconName.swift +++ b/VDS/Components/Icon/IconName.swift @@ -48,6 +48,7 @@ extension Icon { internal static let paginationRightCaret = Name(name: "pagination-right-caret") internal static let verizonUp = Name(name: "verizon-up") internal static let warningBold = Name(name: "warning-bold") + public static let calendar = Name(name: "calendar") public static let checkmark = Name(name: "checkmark") public static let checkmarkAlt = Name(name: "checkmark-alt") public static let close = Name(name: "close") From cd0b06670145bd273e77739bcc42fad069c8e037 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 11:19:57 -0500 Subject: [PATCH 64/87] refactored entryfield base Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 41 +++++++++++------ .../TextFields/EntryFieldBase.swift | 46 ++++++++----------- .../InputField/FieldTypes/FieldType.swift | 8 ---- .../TextFields/InputField/InputField.swift | 37 ++++++++++++--- 4 files changed, 78 insertions(+), 54 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index bf1603a9..411f8327 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -72,6 +72,18 @@ open class DropdownSelect: EntryFieldBase { internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 internal var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } + + /// The is used for the for adding the helperLabel to the right of the containerView. + internal var horizontalStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.spacing = VDSLayout.space3X + $0.alignment = .top + } + }() + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -190,14 +202,6 @@ open class DropdownSelect: EntryFieldBase { dropdownField.isUserInteractionEnabled = isReadOnly ? false : true selectedOptionLabel.surface = surface selectedOptionLabel.isEnabled = isEnabled - - //set the width constraints - let maxwidth = frame.size.width - if let width, width > minWidth && width < maxwidth { - widthConstraint?.constant = width - } else { - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } } /// Resets to default settings. @@ -280,6 +284,7 @@ open class DropdownSelect: EntryFieldBase { open override func updateHelperLabel(){ //remove first + secondaryStackView.removeFromSuperview() helperLabel.removeFromSuperview() super.updateHelperLabel() @@ -287,14 +292,24 @@ open class DropdownSelect: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - middleStackView.spacing = VDSLayout.space3X - middleStackView.distribution = .fillEqually - middleStackView.addArrangedSubview(helperLabel) + horizontalStackView.addArrangedSubview(secondaryStackView) + horizontalStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(horizontalStackView) } else { - middleStackView.spacing = 0 - middleStackView.distribution = .fill bottomContainerStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(secondaryStackView) } + } else { + primaryStackView.addArrangedSubview(secondaryStackView) + } + + //set the width constraints + let frameWidth = frame.size.width + let maxwidth = helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth + if let width, width > minWidth && width < maxwidth { + widthConstraint?.constant = width + } else { + widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index f34a2259..6dca8a0e 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -40,7 +40,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var stackView: UIStackView = { + internal var primaryStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical @@ -48,22 +48,23 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { $0.alignment = .leading } }() - - internal var middleStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.distribution = .fill - $0.alignment = .top - } - }() + internal let secondaryStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + } + + /// This is the view that will be wrapped with the border for userInteraction. internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() + /// This is a horizontal Stack View that is placed inside the containterView (bordered view) + /// The first arrangedView will be the view from getFieldContainer() + /// The second view is the statusIcon. internal var fieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -73,6 +74,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() + /// This is a vertical stack used for the errorLabel and helperLabel. internal var bottomContainerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -207,22 +209,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { super.setup() isAccessibilityElement = false - addSubview(stackView) + addSubview(primaryStackView) //create the wrapping view heightConstraint = containerView.heightGreaterThanEqualTo(constant: containerSize.height) widthConstraint = containerView.width(constant: 0) - let leftStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .vertical - $0.distribution = .fill - } - leftStackView.addArrangedSubview(containerView) - leftStackView.setCustomSpacing(8, after: containerView) - - //add the containerView to the middleStack - middleStackView.addArrangedSubview(leftStackView) + secondaryStackView.addArrangedSubview(containerView) + secondaryStackView.setCustomSpacing(8, after: containerView) //add ContainerStackView //this is the horizontal stack that contains @@ -246,13 +240,13 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(middleStackView) - leftStackView.addArrangedSubview(bottomContainer) + primaryStackView.addArrangedSubview(titleLabel) + primaryStackView.addArrangedSubview(secondaryStackView) + secondaryStackView.addArrangedSubview(bottomContainer) - stackView.setCustomSpacing(4, after: titleLabel) + primaryStackView.setCustomSpacing(4, after: titleLabel) - stackView + primaryStackView .pinTop() .pinLeading() .pinTrailing(0, .defaultHigh) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index db45d767..ebecb17d 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -80,14 +80,6 @@ extension InputField { inputField.fieldStackView.setCustomSpacing(0, after: inputField.statusIcon) } - //set the width constraints - let maxwidth = inputField.frame.size.width - if let width = inputField.width, width > minWidth && width < maxwidth { - inputField.widthConstraint?.constant = width - } else { - inputField.widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } - //placeholder inputField.textField.placeholder = placeholderText diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index e11d1253..08af1134 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -36,6 +36,17 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- internal var titleLabelWidthConstraint: NSLayoutConstraint? + /// The is used for the for adding the helperLabel to the right of the containerView. + internal var horizontalStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.spacing = VDSLayout.space3X + $0.alignment = .top + } + }() + //-------------------------------------------------- // MARK: - Public FieldType Properties //-------------------------------------------------- @@ -173,8 +184,8 @@ open class InputField: EntryFieldBase { textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self - stackView.addArrangedSubview(successLabel) - stackView.setCustomSpacing(8, after: successLabel) + primaryStackView.addArrangedSubview(successLabel) + primaryStackView.setCustomSpacing(8, after: successLabel) fieldStackView.addArrangedSubview(actionTextLink) @@ -248,6 +259,7 @@ open class InputField: EntryFieldBase { } open override func updateHelperLabel(){ //remove first + secondaryStackView.removeFromSuperview() helperLabel.removeFromSuperview() super.updateHelperLabel() @@ -255,14 +267,25 @@ open class InputField: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - middleStackView.spacing = VDSLayout.space3X - middleStackView.distribution = .fillEqually - middleStackView.addArrangedSubview(helperLabel) + horizontalStackView.addArrangedSubview(secondaryStackView) + horizontalStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(horizontalStackView) } else { - middleStackView.spacing = 0 - middleStackView.distribution = .fill bottomContainerStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(secondaryStackView) } + } else { + primaryStackView.addArrangedSubview(secondaryStackView) + } + + //set the width constraints + let minWidth = fieldType.handler().minWidth + let frameWidth = frame.size.width + let maxwidth = helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth + if let width, width > minWidth && width < maxwidth { + widthConstraint?.constant = width + } else { + widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth } } From 3ab8cec7a42e9e8e232054849bc8016d43c418ad Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 11:26:12 -0500 Subject: [PATCH 65/87] added more documentation Signed-off-by: Matt Bruce --- VDS/Components/TextFields/EntryFieldBase.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 6dca8a0e..86cc6a48 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -49,6 +49,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() + /// This is the veritcal stack view that has 2 rows, the containerView and the return view + /// of the getBottomContainer() method, by default returns the bottomContainerStackView. internal let secondaryStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical @@ -56,6 +58,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } /// This is the view that will be wrapped with the border for userInteraction. + /// The only subview of this view is the fieldStackView internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false From 4b5bbd1ace69cd800b9c1cfd975b9c785f470cb3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 12:16:18 -0500 Subject: [PATCH 66/87] got picker working again Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 8 +- VDS/Components/DatePicker/DatePicker.swift | 183 +++++++++++++++++++++ 2 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 VDS/Components/DatePicker/DatePicker.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index d04570b2..a8befef3 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -150,7 +150,7 @@ EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C132BED0DEC00BA39FA /* Number.swift */; }; EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C152BED0E0300BA39FA /* InlineAction.swift */; }; EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; - EAC58C212BF127FE00BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C202BF127FE00BA39FA /* DatePicker.swift */; }; + EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -356,7 +356,7 @@ EAC58C132BED0DEC00BA39FA /* Number.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Number.swift; sourceTree = ""; }; EAC58C152BED0E0300BA39FA /* InlineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAction.swift; sourceTree = ""; }; EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; - EAC58C202BF127FE00BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatePicker.swift; path = ../../../../../../../../../Downloads/DatePicker.swift; sourceTree = ""; }; + EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -915,7 +915,7 @@ EAC58C1F2BF127F000BA39FA /* DatePicker */ = { isa = PBXGroup; children = ( - EAC58C202BF127FE00BA39FA /* DatePicker.swift */, + EAC58C222BF2824200BA39FA /* DatePicker.swift */, ); path = DatePicker; sourceTree = ""; @@ -1168,7 +1168,7 @@ 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, - EAC58C212BF127FE00BA39FA /* DatePicker.swift in Sources */, + EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */, EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */, 71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift new file mode 100644 index 00000000..9d049613 --- /dev/null +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -0,0 +1,183 @@ +import Foundation +import UIKit +import VDSTokens +import Combine + +/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. +@objc(VDSDatePicker) +open class DatePicker: EntryFieldBase { + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// A callback when the selected option changes. Passes parameters (option). + open var onDateSelected: ((Date, DatePicker) -> Void)? + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var minWidthDefault = 186.0 + + internal let picker = UIDatePicker() + + internal var bottomStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.alignment = .top + $0.spacing = VDSLayout.space2X + } + }() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var calendarIcon = Icon().with { + $0.name = .calendar + $0.size = .medium + } + + open var selectedDate: Date? { didSet { setNeedsUpdate() } } + + open override var value: String? { + get { selectedDateLabel.text } + set { } + } + + open var selectedDateLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textAlignment = .left + $0.textStyle = .bodyLarge + $0.lineBreakMode = .byCharWrapping + } + + public enum DateFormat: String, CaseIterable { + case shortNumeric + case longAlphabetic + case mediumNumeric + case consiseNumeric + + public var format: String { + switch self { + case .shortNumeric: "MM/dd/yy" + case .longAlphabetic: "MMMM d, yyyy" + case .mediumNumeric: "MM/dd/yyyy" + case .consiseNumeric: "M/d/yyyy" + } + } + } + + open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + + /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. + open override func setup() { + super.setup() + + accessibilityLabel = "Dropdown Select" + + // setting color config + selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() + + // setup the calendar + picker.datePickerMode = .date + picker.preferredDatePickerStyle = .inline + picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) + picker.isHidden = true + + // tap gesture + fieldStackView + .publisher(for: UITapGestureRecognizer()) + .sink { [weak self] _ in + self?.togglePicker() + } + .store(in: &subscribers) + } + + open override func getFieldContainer() -> UIView { + // stackview for controls in EntryFieldBase.controlContainerView + let controlStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.spacing = VDSLayout.space3X + } + controlStackView.addArrangedSubview(calendarIcon) + controlStackView.addArrangedSubview(selectedDateLabel) + return controlStackView + } + + open override func getBottomContainer() -> UIView { + bottomStackView.addArrangedSubview(bottomContainerStackView) + bottomStackView.addArrangedSubview(picker) + return bottomStackView + } + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + + if let selectedDate { + picker.date = selectedDate + formatDate(selectedDate) + } + + selectedDateLabel.surface = surface + selectedDateLabel.isEnabled = isEnabled + calendarIcon.color = iconColorConfiguration.getColor(self) + + //set the width constraints + let minWidth = containerSize.width + let maxwidth = frame.size.width + if let width, width > minWidth && width < maxwidth { + widthConstraint?.constant = width + } else { + widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth + } + } + + /// Resets to default settings. + open override func reset() { + super.reset() + selectedDateLabel.textStyle = .bodyLarge + } + + internal func formatDate(_ date: Date) { + let formatter = DateFormatter() + formatter.dateFormat = dateFormat.format + selectedDateLabel.text = formatter.string(from: date) + } + + internal func togglePicker() { + picker.isHidden = !picker.isHidden + bottomContainerStackView.isHidden = !bottomContainerStackView.isHidden + } + + @objc private func dateChanged(_ sender: UIDatePicker) { + selectedDate = sender.date + sendActions(for: .valueChanged) + togglePicker() + } +} From e667054eb636df3b5d07a68e4a048179fdcc0540 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 12:32:46 -0500 Subject: [PATCH 67/87] refactored calculating width Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 9 --------- .../DropdownSelect/DropdownSelect.swift | 20 ++++++++----------- .../TextFields/EntryFieldBase.swift | 16 +++++++++++++-- .../TextFields/InputField/InputField.swift | 15 +++++--------- .../TextFields/TextArea/TextArea.swift | 4 ---- 5 files changed, 27 insertions(+), 37 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 9d049613..a7dc3e29 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -147,15 +147,6 @@ open class DatePicker: EntryFieldBase { selectedDateLabel.surface = surface selectedDateLabel.isEnabled = isEnabled calendarIcon.color = iconColorConfiguration.getColor(self) - - //set the width constraints - let minWidth = containerSize.width - let maxwidth = frame.size.width - if let width, width > minWidth && width < maxwidth { - widthConstraint?.constant = width - } else { - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } } /// Resets to default settings. diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 411f8327..9c30ea03 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -71,7 +71,11 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 - internal var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } + internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } + internal override var maxWidth: CGFloat { + let frameWidth = frame.size.width + return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth + } /// The is used for the for adding the helperLabel to the right of the containerView. internal var horizontalStackView: UIStackView = { @@ -133,7 +137,8 @@ open class DropdownSelect: EntryFieldBase { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() - + widthConstraint?.activate() + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabelWidthConstraint = titleLabel.width(constant: 0) @@ -302,17 +307,8 @@ open class DropdownSelect: EntryFieldBase { } else { primaryStackView.addArrangedSubview(secondaryStackView) } - - //set the width constraints - let frameWidth = frame.size.width - let maxwidth = helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth - if let width, width > minWidth && width < maxwidth { - widthConstraint?.constant = width - } else { - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } } - + open override func updateAccessibility() { super.updateAccessibility() let selectedOption = selectedOptionLabel.text ?? "" diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 86cc6a48..c7cd766c 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -93,7 +93,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. - internal var containerSize: CGSize { CGSize(width: 45, height: 44) } + internal var maxWidth: CGFloat { frame.size.width } + internal var minWidth: CGFloat { containerSize.width } + internal var containerSize: CGSize { CGSize(width: minWidth, height: 44) } internal let primaryColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) @@ -216,7 +218,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //create the wrapping view heightConstraint = containerView.heightGreaterThanEqualTo(constant: containerSize.height) - widthConstraint = containerView.width(constant: 0) + widthConstraint = containerView.width(constant: frame.size.width) secondaryStackView.addArrangedSubview(containerView) secondaryStackView.setCustomSpacing(8, after: containerView) @@ -292,6 +294,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { updateTitleLabel() updateErrorLabel() updateHelperLabel() + updateContainerWidth() } open func validate(){ @@ -314,6 +317,15 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- + open func updateContainerWidth() { + if let width, width > minWidth && width < maxWidth { + widthConstraint?.constant = width + } else { + widthConstraint?.constant = maxWidth >= minWidth ? maxWidth : minWidth + } + widthConstraint?.activate() + } + /// Container for the area in which the user interacts. open func getFieldContainer() -> UIView { fatalError("Subclass must return the view that contains the field/view the user will interact with.") diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 08af1134..1886a9ab 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -35,6 +35,11 @@ open class InputField: EntryFieldBase { // MARK: - Private Properties //-------------------------------------------------- internal var titleLabelWidthConstraint: NSLayoutConstraint? + internal override var minWidth: CGFloat { fieldType.handler().minWidth } + internal override var maxWidth: CGFloat { + let frameWidth = frame.size.width + return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth + } /// The is used for the for adding the helperLabel to the right of the containerView. internal var horizontalStackView: UIStackView = { @@ -277,16 +282,6 @@ open class InputField: EntryFieldBase { } else { primaryStackView.addArrangedSubview(secondaryStackView) } - - //set the width constraints - let minWidth = fieldType.handler().minWidth - let frameWidth = frame.size.width - let maxwidth = helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth - if let width, width > minWidth && width < maxwidth { - widthConstraint?.constant = width - } else { - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } } override func updateRules() { diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 3913f2e6..eccd228e 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -187,10 +187,6 @@ open class TextArea: EntryFieldBase { textView.isEnabled = isEnabled textView.surface = surface - //set the width constraints - let maxwidth = frame.size.width - let minWidth = containerSize.width - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth textViewHeightConstraint?.constant = minHeight.value characterCounterLabel.text = getCharacterCounterText() From 28d8161d09aea73fb93093d5af428682073d9fa2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 13:12:07 -0500 Subject: [PATCH 68/87] refactored date picker a popover Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 85 ++++++++++++++++------ 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index a7dc3e29..cd4b3777 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -5,7 +5,7 @@ import Combine /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. @objc(VDSDatePicker) -open class DatePicker: EntryFieldBase { +open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, UIPopoverPresentationControllerDelegate { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -31,9 +31,7 @@ open class DatePicker: EntryFieldBase { // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 186.0 - - internal let picker = UIDatePicker() - + internal var bottomStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -103,10 +101,6 @@ open class DatePicker: EntryFieldBase { selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() // setup the calendar - picker.datePickerMode = .date - picker.preferredDatePickerStyle = .inline - picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) - picker.isHidden = true // tap gesture fieldStackView @@ -128,19 +122,12 @@ open class DatePicker: EntryFieldBase { controlStackView.addArrangedSubview(selectedDateLabel) return controlStackView } - - open override func getBottomContainer() -> UIView { - bottomStackView.addArrangedSubview(bottomContainerStackView) - bottomStackView.addArrangedSubview(picker) - return bottomStackView - } - + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() if let selectedDate { - picker.date = selectedDate formatDate(selectedDate) } @@ -162,13 +149,69 @@ open class DatePicker: EntryFieldBase { } internal func togglePicker() { - picker.isHidden = !picker.isHidden - bottomContainerStackView.isHidden = !bottomContainerStackView.isHidden + let calendarVC = DatePickerPopoverViewController(calendar: Calendar(identifier: .gregorian), delegate: self) + calendarVC.modalPresentationStyle = .popover + calendarVC.selectedDate = selectedDate ?? Date() + if let popoverController = calendarVC.popoverPresentationController { + popoverController.delegate = self + popoverController.sourceView = containerView + popoverController.sourceRect = containerView.bounds + popoverController.permittedArrowDirections = .any + } + if let viewController = UIApplication.topViewController() { + viewController.present(calendarVC, animated: true, completion: nil) + } } - @objc private func dateChanged(_ sender: UIDatePicker) { - selectedDate = sender.date + internal func didSelectDate(_ controller: DatePickerPopoverViewController, date: Date) { + selectedDate = date + controller.dismiss(animated: true) sendActions(for: .valueChanged) - togglePicker() + } + + public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } +} + +protocol DatePickerPopoverViewControllerDelegate: NSObject { + func didSelectDate(_ controller: DatePickerPopoverViewController, date: Date) +} + +class DatePickerPopoverViewController: UIViewController { + + private let picker = UIDatePicker() + weak var delegate: DatePickerPopoverViewControllerDelegate? + + init(calendar: Calendar, delegate: DatePickerPopoverViewControllerDelegate?) { + self.delegate = delegate + super.init(nibName: nil, bundle: nil) + picker.datePickerMode = .date + picker.preferredDatePickerStyle = .inline + picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) + } + + var selectedDate: Date = Date() { + didSet { + picker.date = selectedDate + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = picker + view.backgroundColor = .white + } + + override func viewDidLoad() { + super.viewDidLoad() + preferredContentSize = CGSize(width: 300, height: 400) // Adjust as needed + } + + @objc private func dateChanged(_ sender: UIDatePicker) { + delegate?.didSelectDate(self, date: sender.date) } } From 31b893042c5a26843f53384e3bf88bbf4c2369c1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 13:15:46 -0500 Subject: [PATCH 69/87] updated for isReadOnly Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index cd4b3777..96919781 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -100,13 +100,14 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() - // setup the calendar - // tap gesture fieldStackView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in - self?.togglePicker() + guard let self else { return } + if self.isEnabled && !self.isReadOnly { + self.togglePicker() + } } .store(in: &subscribers) } From 1c5273155df9f03c19daecf709ebd42afe62751c Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 13 May 2024 15:13:24 -0500 Subject: [PATCH 70/87] added changelog Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++ VDS/Components/DatePicker/DatePicker.swift | 6 ++- .../DatePicker/DatePickerChangeLog.txt | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 VDS/Components/DatePicker/DatePickerChangeLog.txt diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index a8befef3..33d4b421 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C152BED0E0300BA39FA /* InlineAction.swift */; }; EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; + EAC58C252BF2A7FB00BA39FA /* DatePickerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -357,6 +358,7 @@ EAC58C152BED0E0300BA39FA /* InlineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAction.swift; sourceTree = ""; }; EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = ""; }; + EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -916,6 +918,7 @@ isa = PBXGroup; children = ( EAC58C222BF2824200BA39FA /* DatePicker.swift */, + EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */, ); path = DatePicker; sourceTree = ""; @@ -1136,6 +1139,7 @@ 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */, EAEEEC9C2B1F8F0700531FC2 /* TextLinkCaretChangeLog.txt in Resources */, EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */, + EAC58C252BF2A7FB00BA39FA /* DatePickerChangeLog.txt in Resources */, 71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */, EAEEECAD2B1FC1A600531FC2 /* TitleLockupChangeLog.txt in Resources */, EAEEECAB2B1FBF2A00531FC2 /* ToggleChangeLog.txt in Resources */, diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 96919781..2d6b620e 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -64,7 +64,7 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, $0.lineBreakMode = .byCharWrapping } - public enum DateFormat: String, CaseIterable { + public enum DateFormat: String, CaseIterable, CustomStringConvertible { case shortNumeric case longAlphabetic case mediumNumeric @@ -78,6 +78,10 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, case .consiseNumeric: "M/d/yyyy" } } + + public var description: String { + return format + } } open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } } diff --git a/VDS/Components/DatePicker/DatePickerChangeLog.txt b/VDS/Components/DatePicker/DatePickerChangeLog.txt new file mode 100644 index 00000000..e588b709 --- /dev/null +++ b/VDS/Components/DatePicker/DatePickerChangeLog.txt @@ -0,0 +1,48 @@ +MM/DD/YYYY +---------------- +Initial Brand 3.0 handoff + +01/06/2021 +---------------- +Removed Max Width, Increased Min width. +Updated the tokens with FormControl tokens + +02/24/2022 +---------------- +Replaced Calendar, Info, Error, Caret Left, and Caret Right Non-Scaling icons with VDS Icon. + +06/22/2022 +---------------- +Updated Calendar instances with new Calendar (with Indicators) + +07/27/2022 +---------------- +Updated Calendar Date Picker Configurations to include Background Transparent Boolean. +Moved Default Selection Date, Always opened and Date formats to configurartions page. + +08/04/2022 +---------------- +Updated default and inverted prop to light and dark surface. + +09/09/2022 +---------------- +Added missing helper text from disabled state visuals. + +12/13/2022 +---------------- +Updated supported date formats +Added "(web only)" to any instance of "keyboard focus" +Replaced form border and focus border pixel values, styles & spacing with tokens. + +01/20/2023 +---------------- +Updated Specs to use new SPEC Templates and SPEC DOC Components. +Tweaked written out date format so Mo becomes Mon + +04/12/2023 +---------------- +Updated hex colors for updated feedback tokens in error states. + +02/08/2024 +---------------- +Added Calendar position section to Behaviors. From 8fb424bed9b66070b12d0ed829a32663ab9309de Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 14 May 2024 15:23:07 +0530 Subject: [PATCH 71/87] Digital ACT-191 ONEAPP-7958 story: added active state to highlight date in selecting date --- VDS/Components/Calendar/Calendar.swift | 12 ++++++++++++ .../Calendar/CalendarDateViewCell.swift | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 9ec8e6fc..ff137004 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -296,11 +296,23 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI return UICollectionReusableView() } + + public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { + let isEnabled: Bool = cell.isDateEnabled() + if isEnabled { + cell.activeModeStart() + } + } + return true + } + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // reload selected index, if it is in enabled state. if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { let isEnabled: Bool = cell.isDateEnabled() if isEnabled { + cell.activeModeEnd() // Callback to pass selected date if it is enabled only. let selectedItem = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! onChangeSelectedDate?(selectedItem) diff --git a/VDS/Components/Calendar/CalendarDateViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift index 26886d0a..29bcb730 100644 --- a/VDS/Components/Calendar/CalendarDateViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -42,7 +42,7 @@ final class CalendarDateViewCell: UICollectionViewCell { } private lazy var shapeLayer = CAShapeLayer() - + private var surface: Surface = .light private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) private let selectedBackgroundColor = SurfaceColorConfiguration(VDSColor.backgroundPrimaryInverseLight, VDSColor.backgroundPrimaryInverseDark) private let selectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteGray65, VDSColor.paletteGray44) @@ -50,7 +50,7 @@ final class CalendarDateViewCell: UICollectionViewCell { private let unselectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) private let disabledTextColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark) private let disabledBackgroundColor = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) - + private var activeBorderColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.borderHoverOnlight , VDSFormControlsColor.borderHoverOndark) private let currentDate = Date() //-------------------------------------------------- @@ -100,6 +100,7 @@ final class CalendarDateViewCell: UICollectionViewCell { stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } numberLabel.surface = surface numberLabel.text = text + self.surface = surface // enable/disable cells based on min date, max date and active/inactive dates. self.updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) @@ -152,6 +153,18 @@ final class CalendarDateViewCell: UICollectionViewCell { return numberLabel.isEnabled } + func activeModeStart() { + numberLabel.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor + numberLabel.layer.borderWidth = VDSFormControls.borderWidth + numberLabel.layer.cornerRadius = VDSFormControls.borderRadius + } + + func activeModeEnd() { + numberLabel.layer.borderColor = nil + numberLabel.layer.borderWidth = 0 + numberLabel.layer.cornerRadius = 0 + } + func disableLabel(with surface: Surface) { numberLabel.isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) From f30d416164903dba1ac1658e3fbf5d67d7663394 Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 14 May 2024 15:24:51 +0530 Subject: [PATCH 72/87] Digital ACT-191 ONEAPP-7958 story: adding a border to the collectionView instead of the CalendarBase.layer --- VDS/Components/Calendar/Calendar.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index ff137004..d980d0b3 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -158,15 +158,15 @@ open class CalendarBase: View { self.fetchDates(with: displayDate) } - layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor + containerView.layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor if hideContainerBorder { - layer.borderColor = nil - layer.borderWidth = 0 - layer.cornerRadius = 0 + containerView.layer.borderColor = nil + containerView.layer.borderWidth = 0 + containerView.layer.cornerRadius = 0 } else { - layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor - layer.borderWidth = VDSFormControls.borderWidth - layer.cornerRadius = VDSFormControls.borderRadius + containerView.layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor + containerView.layer.borderWidth = VDSFormControls.borderWidth + containerView.layer.cornerRadius = VDSFormControls.borderRadius } } From a440ba48443bf00a18c5750609e0bea3b445076a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 09:00:21 -0500 Subject: [PATCH 73/87] added external link icon Signed-off-by: Matt Bruce --- .../external-link.imageset/Contents.json | 15 +++++++++++++++ .../external-link.imageset/external-link.svg | 1 + 2 files changed, 16 insertions(+) create mode 100644 VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json create mode 100644 VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg diff --git a/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json new file mode 100644 index 00000000..1e10c5e2 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "external-link.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg new file mode 100644 index 00000000..5c545b6e --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file From 05829907a9a5e68c5466126ae9b828f499d9149b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 09:00:36 -0500 Subject: [PATCH 74/87] updated icon models for color and icon type Signed-off-by: Matt Bruce --- .../Tilelet/TileletIconModels.swift | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/VDS/Components/Tilelet/TileletIconModels.swift b/VDS/Components/Tilelet/TileletIconModels.swift index 457cd907..788a2155 100644 --- a/VDS/Components/Tilelet/TileletIconModels.swift +++ b/VDS/Components/Tilelet/TileletIconModels.swift @@ -7,6 +7,7 @@ import Foundation import UIKit +import VDSTokens extension Tilelet { @@ -15,17 +16,21 @@ extension Tilelet { /// A representation that will be used to render the icon with corresponding name. public var name: Icon.Name + /// Color of the icon. + public var color: UIColor + /// Enum for a preset height and width for the icon. public var size: Icon.Size /// Accessible Text for the Icon - var accessibleText: String + public var accessibleText: String /// Current Surface and this is used to pass down to child objects that implement Surfacable public var surface: Surface - public init(name: Icon.Name = .multipleDocuments, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) { + public init(name: Icon.Name = .multipleDocuments, color: UIColor = VDSColor.paletteBlack, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) { self.name = name + self.color = color self.accessibleText = accessibleText ?? name.rawValue self.size = size self.surface = surface @@ -34,13 +39,34 @@ extension Tilelet { /// Model that represents the options available for the directional icon. public struct DirectionalIcon { + public enum IconType { + case rightArrow + case externalLink + + public var iconName: Icon.Name { + return self == .rightArrow ? .rightArrow : .externalLink + } + } + + /// Color of the icon. + public var color: UIColor + + /// Accessible Text for the Icon + public var accessibleText: String + + /// Enum for a icon type you want shown.. + public var iconType: IconType + /// Enum for a preset height and width for the icon. public var size: Icon.Size - + /// Current Surface and this is used to pass down to child objects that implement Surfacable public var surface: Surface - public init(size: Icon.Size = .medium, surface: Surface = .dark) { + public init(iconType: IconType = .rightArrow, color: UIColor = VDSColor.paletteBlack, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) { + self.iconType = iconType + self.color = color + self.accessibleText = accessibleText ?? iconType.iconName.rawValue self.size = size self.surface = surface } From b31ff5c9619e305c98c1a787406eb64f46cc8699 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 09:00:54 -0500 Subject: [PATCH 75/87] added iconName for the new asset Signed-off-by: Matt Bruce --- VDS/Components/Icon/IconName.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VDS/Components/Icon/IconName.swift b/VDS/Components/Icon/IconName.swift index 27f57787..6af40d3d 100644 --- a/VDS/Components/Icon/IconName.swift +++ b/VDS/Components/Icon/IconName.swift @@ -55,6 +55,7 @@ extension Icon { public static let downCaret = Name(name: "down-caret") public static let downCaretBold = Name(name: "down-caret-bold") public static let error = Name(name: "error") + public static let externalLink = Icon.Name(name: "external-link") public static let info = Name(name: "info") public static let multipleDocuments = Name(name: "multiple-documents") public static let leftArrow = Name(name: "left-arrow") @@ -62,5 +63,6 @@ extension Icon { public static let rightArrow = Name(name: "right-arrow") public static let rightCaret = Name(name: "right-caret") public static let warning = Name(name: "warning") + } } From 16a4bb28ace7bdbcb13ee342caab6c1729dbcfec Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 09:01:04 -0500 Subject: [PATCH 76/87] updated model changes in tilelet Signed-off-by: Matt Bruce --- VDS/Components/Tilelet/Tilelet.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VDS/Components/Tilelet/Tilelet.swift b/VDS/Components/Tilelet/Tilelet.swift index f0814e11..98ce49aa 100644 --- a/VDS/Components/Tilelet/Tilelet.swift +++ b/VDS/Components/Tilelet/Tilelet.swift @@ -532,6 +532,7 @@ open class Tilelet: TileContainerBase { var showIconContainerView = false if let descriptiveIconModel { descriptiveIcon.name = descriptiveIconModel.name + descriptiveIcon.color = descriptiveIconModel.color descriptiveIcon.size = descriptiveIconModel.size descriptiveIcon.surface = backgroundColorSurface descriptiveIcon.accessibilityLabel = descriptiveIconModel.accessibleText @@ -539,9 +540,11 @@ open class Tilelet: TileContainerBase { } if let directionalIconModel { + directionalIcon.name = directionalIconModel.iconType.iconName + directionalIcon.color = directionalIconModel.color directionalIcon.size = directionalIconModel.size directionalIcon.surface = backgroundColorSurface - directionalIcon.accessibilityLabel = "Right arrow" + directionalIcon.accessibilityLabel = directionalIconModel.accessibleText showIconContainerView = true } From 6f17c1b0dab09e4be8a272d7fbea6330ec30ce14 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 11:23:00 -0500 Subject: [PATCH 77/87] refactored to use new calendar Signed-off-by: Matt Bruce --- VDS/Components/Calendar/Date+Extension.swift | 2 +- VDS/Components/DatePicker/DatePicker.swift | 195 +++++++++++++++---- 2 files changed, 159 insertions(+), 38 deletions(-) diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 8d697049..ca4f5a3b 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -7,7 +7,7 @@ import Foundation -extension Date { +public extension Date { static var firstDayOfWeek = Calendar.current.firstWeekday diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 2d6b620e..710de91a 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -52,6 +52,10 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, open var selectedDate: Date? { didSet { setNeedsUpdate() } } + open var calendarModel: CalendarModel = .init() { + didSet { setNeedsUpdate() } + } + open override var value: String? { get { selectedDateLabel.text } set { } @@ -154,14 +158,15 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, } internal func togglePicker() { - let calendarVC = DatePickerPopoverViewController(calendar: Calendar(identifier: .gregorian), delegate: self) + //let calendarVC = DatePickerPopoverViewController(calendar: Calendar(identifier: .gregorian), delegate: self) + let calendarVC = DatePickerPopoverViewController(calendarModel, delegate: self) calendarVC.modalPresentationStyle = .popover calendarVC.selectedDate = selectedDate ?? Date() if let popoverController = calendarVC.popoverPresentationController { popoverController.delegate = self popoverController.sourceView = containerView popoverController.sourceRect = containerView.bounds - popoverController.permittedArrowDirections = .any + popoverController.permittedArrowDirections = .up } if let viewController = UIApplication.topViewController() { viewController.present(calendarVC, animated: true, completion: nil) @@ -179,44 +184,160 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, } } -protocol DatePickerPopoverViewControllerDelegate: NSObject { - func didSelectDate(_ controller: DatePickerPopoverViewController, date: Date) -} - -class DatePickerPopoverViewController: UIViewController { - - private let picker = UIDatePicker() - weak var delegate: DatePickerPopoverViewControllerDelegate? - - init(calendar: Calendar, delegate: DatePickerPopoverViewControllerDelegate?) { - self.delegate = delegate - super.init(nibName: nil, bundle: nil) - picker.datePickerMode = .date - picker.preferredDatePickerStyle = .inline - picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) - } - - var selectedDate: Date = Date() { - didSet { - picker.date = selectedDate +extension DatePicker { + public struct CalendarModel { + public let surface: Surface + + /// If set to true, the calendar will not have a border. + public let hideContainerBorder: Bool + + /// If set to true, the calendar will not have current date indication. + public let hideCurrentDateIndicator: Bool + + /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be inactive. + public let activeDates: [Date] + + /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be active. + public let inactiveDates: [Date] + + /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. + public let minDate: Date + + /// If provided, the calendar will allow a selection to be made up to this date. + public let maxDate: Date + + /// If provided, this is the date that will show as selected by the Calendar. + /// If no value is provided, the current date will be used. If null is provided, no date will be selected. + public let selectedDate: Date + + /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. + public let indicators: [CalendarBase.CalendarIndicatorModel] + + public init(surface: Surface = .light, + hideContainerBorder: Bool = false, + hideCurrentDateIndicator: Bool = false, + selectedDate: Date = Date(), + activeDates: [Date] = [], + inactiveDates: [Date] = [], + minDate: Date = Date().startOfMonth, + maxDate: Date = Date().endOfMonth, + indicators: [CalendarBase.CalendarIndicatorModel] = []) { + self.surface = surface + self.hideContainerBorder = hideContainerBorder + self.hideCurrentDateIndicator = hideCurrentDateIndicator + self.selectedDate = selectedDate + self.activeDates = activeDates + self.inactiveDates = inactiveDates + self.minDate = minDate + self.maxDate = maxDate + self.indicators = indicators } } +} - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } +protocol DatePickerPopoverViewControllerDelegate: NSObject { + func didSelectDate(_ controller: DatePicker.DatePickerPopoverViewController, date: Date) +} - override func loadView() { - view = picker - view.backgroundColor = .white - } - - override func viewDidLoad() { - super.viewDidLoad() - preferredContentSize = CGSize(width: 300, height: 400) // Adjust as needed - } - - @objc private func dateChanged(_ sender: UIDatePicker) { - delegate?.didSelectDate(self, date: sender.date) +//class DatePickerPopoverViewController: UIViewController { +// +// private let picker = UIDatePicker() +// weak var delegate: DatePickerPopoverViewControllerDelegate? +// +// init(calendar: Calendar, delegate: DatePickerPopoverViewControllerDelegate?) { +// self.delegate = delegate +// super.init(nibName: nil, bundle: nil) +// picker.datePickerMode = .date +// picker.preferredDatePickerStyle = .inline +// picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) +// } +// +// var selectedDate: Date = Date() { +// didSet { +// picker.date = selectedDate +// } +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// override func viewDidLoad() { +// super.viewDidLoad() +// let v = UIView().with { +// $0.translatesAutoresizingMaskIntoConstraints = false +// $0.backgroundColor = .white +// $0.width(constant: 250) +// $0.height(constant: 350) +// } +// view.addSubview(v) +// v.pinTop(25).pinLeading(15).pinTrailing(15).pinBottom(15) +// +// view.backgroundColor = .blue +// +// preferredContentSize = CGSize(width: 300, height: 400) // Adjust as needed +// } +// +// @objc private func dateChanged(_ sender: UIDatePicker) { +// delegate?.didSelectDate(self, date: sender.date) +// } +//} +extension DatePicker { + class DatePickerPopoverViewController: UIViewController { + private var padding: CGFloat = 15 + private var topPadding: CGFloat { 10 + padding } + private var calendarModel: CalendarModel + private let picker = CalendarBase() + weak var delegate: DatePickerPopoverViewControllerDelegate? + + init(_ calendarModel: CalendarModel, delegate: DatePickerPopoverViewControllerDelegate?) { + self.delegate = delegate + self.calendarModel = calendarModel + super.init(nibName: nil, bundle: nil) + self.picker.onChangeSelectedDate = { [weak self] date in + guard let self else { return } + self.delegate?.didSelectDate(self, date: date) + } + } + + var selectedDate: Date = Date() { + didSet { + picker.selectedDate = selectedDate + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(picker) + picker.surface = calendarModel.surface + picker.hideContainerBorder = calendarModel.hideContainerBorder + picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator + picker.activeDates = calendarModel.activeDates + picker.inactiveDates = calendarModel.inactiveDates + picker.selectedDate = calendarModel.selectedDate + picker.indicators = calendarModel.indicators + picker.minDate = calendarModel.minDate + picker.maxDate = calendarModel.maxDate + picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) + view.backgroundColor = picker.backgroundColor + } + + override var preferredContentSize: CGSize { + get { + var size = picker.containerSize + size.height += 40 + size.width += 30 + return size + } + set { + super.preferredContentSize = newValue + } + } } } From 561b47610bebedebaf85fb6ce79c910b24d021e2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 13:24:05 -0500 Subject: [PATCH 78/87] updated cornerRadius Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 08b87a7f..ffd16496 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -187,7 +187,7 @@ open class TileContainerBase: Control where Padding //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- - private let cornerRadius = VDSFormControls.borderRadius * 2 + private let cornerRadius = VDSLayout.shapeCornerradiusTiles internal var backgroundColorConfiguration = BackgroundColorConfiguration() private let dropShadowConfiguration = DropShadowConfiguration().with { $0.shadowColorConfiguration = SurfaceColorConfiguration().with { From 49ec99e9bed479b75664a5b8e763adb653809363 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 16:20:00 -0500 Subject: [PATCH 79/87] refactored code Signed-off-by: Matt Bruce --- VDS/Components/Calendar/Calendar.swift | 151 ++++++++++-------- .../Calendar/CalendarDateViewCell.swift | 89 ++++++----- .../Calendar/CalendarFooterReusableView.swift | 7 +- .../Calendar/CalendarHeaderReusableView.swift | 4 +- VDS/Components/Calendar/Date+Extension.swift | 35 ++-- 5 files changed, 159 insertions(+), 127 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 9ec8e6fc..18465c47 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -69,7 +69,8 @@ open class CalendarBase: View { // MARK: - Private Properties //-------------------------------------------------- internal var containerSize: CGSize { CGSize(width: 328, height: 376) } - + internal var calendar = Calendar.current + private let cellItemSize = CGSize(width: 40, height: 40) private let headerHeight = 88.0 private let footerHeight = 40.0 @@ -96,10 +97,14 @@ open class CalendarBase: View { collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false collectionView.backgroundColor = .clear - collectionView.register(CalendarDateViewCell.self, forCellWithReuseIdentifier: CalendarDateViewCell.identifier) + + collectionView.register(CalendarDateViewCell.self, + forCellWithReuseIdentifier: CalendarDateViewCell.identifier) + collectionView.register(CalendarHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CalendarHeaderReusableView.identifier) + collectionView.register(CalendarFooterReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: CalendarFooterReusableView.identifier) @@ -138,6 +143,7 @@ open class CalendarBase: View { containerView.addSubview(collectionView) let calendarHeight = containerSize.height - (2 * VDSLayout.space4X) let spacing = (containerSize.width - calendarWidth) / 2 + collectionView .pinTop(VDSLayout.space4X) .pinBottom(VDSLayout.space4X) @@ -145,7 +151,8 @@ open class CalendarBase: View { .pinTrailing(spacing) .width(calendarWidth) .heightGreaterThanEqualTo(calendarHeight) - collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() + + collectionView.pinCenterX(anchor: containerView.centerXAnchor) } open override func updateView() { @@ -155,7 +162,7 @@ open class CalendarBase: View { // Check if current date falls between min & max dates. let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate) displayDate = fallsBetween ? displayDate : minDate - self.fetchDates(with: displayDate) + fetchDates(with: displayDate) } layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor @@ -169,11 +176,7 @@ open class CalendarBase: View { layer.cornerRadius = VDSFormControls.borderRadius } } - - override open func layoutSubviews() { - super.layoutSubviews() - } - + /// Resets to default settings. open override func reset() { super.reset() @@ -188,41 +191,31 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- - func fetchDates(with aDate:Date) { + func fetchDates(with aDate: Date) { heightConstraint?.isActive = false containerHeightConstraint?.isActive = false days.removeAll() - self.dates = aDate.calendarDisplayDays + dates = aDate.calendarDisplayDays + for date in dates { // code to be executed if date.monthInt != aDate.monthInt { days.append("") } else { - days.append(getDay(with: date)) + days.append(date.getDay()) } } - self.collectionView.reloadData() - var height = self.collectionView.collectionViewLayout.collectionViewContentSize.height - height = (height > 0) ? height : containerSize.height + + collectionView.reloadData() + + var height = collectionView.collectionViewLayout.collectionViewContentSize.height + height = height > 0 ? height : containerSize.height heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: height) - containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: (height + (2 * VDSLayout.space4X))) + containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: height + (2 * VDSLayout.space4X)) heightConstraint?.isActive = true containerHeightConstraint?.isActive = true - self.layoutIfNeeded() - } - - func getDay(with date:Date) -> String { - if #available(iOS 15.0, *) { - return date.formatted(.dateTime.day()) - } else { - // Fallback on earlier versions - let dateFormatter: DateFormatter = DateFormatter() - dateFormatter.dateFormat = "d" - let day: String = dateFormatter.string(from: date) - return day - } - } - + layoutIfNeeded() + } } extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { @@ -235,21 +228,41 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateViewCell.identifier, for: indexPath) as? CalendarDateViewCell else { return UICollectionViewCell() } + var indicatorCount = 0 - if self.indicators.count > 0 { - for x in (0...(self.indicators.count-1)) { - if (self.days[indexPath.row] == self.getDay(with: self.indicators[x].date)) { + + if indicators.count > 0 { + for x in 0...indicators.count - 1 { + if days[indexPath.row] == indicators[x].date.getDay() { indicatorCount += 1 } } } - cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, displayDate: displayDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) - if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } + + cell.update(with: surface, + indicators: indicators, + text: days[indexPath.row], + indicatorCount: indicatorCount, + selectedDate: selectedDate, + displayDate: displayDate, + hideDate: hideCurrentDateIndicator, + minDate: minDate, + maxDate: maxDate, + activeDates: activeDates, + inactiveDates: inactiveDates) + + if days[indexPath.row] == selectedDate.getDay() { + selectedIndexPath = indexPath + } + return cell } public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { + + switch kind { + case UICollectionView.elementKindSectionHeader: + // Header guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { return UICollectionReusableView() @@ -258,42 +271,51 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI var prevEnabled = false // check the interval between min date, max date.. set enable/disable flag for next / previous buttons. - if ((displayDate.monthInt < maxDate.monthInt) && (displayDate.yearInt == maxDate.yearInt)) || (displayDate.yearInt < maxDate.yearInt) { + if displayDate.monthInt < maxDate.monthInt && displayDate.yearInt == maxDate.yearInt + || displayDate.yearInt < maxDate.yearInt { nextEnabled = true } - if ((minDate.monthInt < displayDate.monthInt) && (minDate.yearInt == displayDate.yearInt)) || (minDate.yearInt < displayDate.yearInt) { + if minDate.monthInt < displayDate.monthInt && minDate.yearInt == displayDate.yearInt + || minDate.yearInt < displayDate.yearInt { prevEnabled = true } header.nextClicked = { [weak self] in guard let self = self else { return } - let aDate = Calendar.current.date(byAdding: .month, value: 1, to:self.displayDate)! - if ((aDate.monthInt <= maxDate.monthInt) && (aDate.yearInt == maxDate.yearInt)) || (aDate.yearInt < maxDate.yearInt) { - displayDate = aDate - self.fetchDates(with: displayDate) + let date = calendar.date(byAdding: .month, value: 1, to:displayDate)! + if date.monthInt <= maxDate.monthInt && date.yearInt == maxDate.yearInt + || date.yearInt < maxDate.yearInt { + displayDate = date + fetchDates(with: displayDate) } } + header.previousClicked = { [weak self] in guard let self = self else { return } - let aDate = Calendar.current.date(byAdding: .month, value: -1, to:self.displayDate)! - if ((minDate.monthInt <= aDate.monthInt) && (minDate.yearInt == aDate.yearInt)) || (minDate.yearInt < aDate.yearInt) { - displayDate = aDate - self.fetchDates(with: displayDate) + let date = calendar.date(byAdding: .month, value: -1, to:displayDate)! + if minDate.monthInt <= date.monthInt && minDate.yearInt == date.yearInt || (minDate.yearInt < date.yearInt) { + displayDate = date + fetchDates(with: displayDate) } } - header.update(with: surface, aDate: displayDate, nextEnabled: nextEnabled, previousEnabled: prevEnabled) + header.update(with: surface, date: displayDate, nextEnabled: nextEnabled, previousEnabled: prevEnabled) + return header - } else { - // Footer - if kind == UICollectionView.elementKindSectionFooter { - guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { - return UICollectionReusableView() - } - footer.update(with: surface, indicators: indicators) - return footer + + case UICollectionView.elementKindSectionFooter: + + guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { + return UICollectionReusableView() } + footer.update(with: surface, indicators: indicators) + + return footer + + default: + + return UICollectionReusableView() + } - return UICollectionReusableView() } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { @@ -301,18 +323,21 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { let isEnabled: Bool = cell.isDateEnabled() if isEnabled { - // Callback to pass selected date if it is enabled only. - let selectedItem = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! - onChangeSelectedDate?(selectedItem) - selectedDate = self.dates[indexPath.row] + // Callback to pass selected date if it is enabled only. + selectedDate = dates[indexPath.row] + onChangeSelectedDate?(selectedDate) displayDate = selectedDate + var reloadIndexPaths = [indexPath] // If an cell is already selected, then it needs to be deselected. // Add its index path to the array of index paths to be reloaded. - if let deselectIndexPath = selectedIndexPath { reloadIndexPaths.append(deselectIndexPath) } - self.collectionView.reloadItems(at: reloadIndexPaths) + if let deselectIndexPath = selectedIndexPath { + reloadIndexPaths.append(deselectIndexPath) + } + + collectionView.reloadItems(at: reloadIndexPaths) } } } diff --git a/VDS/Components/Calendar/CalendarDateViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift index 26886d0a..b9f7359b 100644 --- a/VDS/Components/Calendar/CalendarDateViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -80,7 +80,7 @@ final class CalendarDateViewCell: UICollectionViewCell { .pinTrailingLessThanOrEqualTo() .height(containerSize.height) .width(containerSize.width) - containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + .pinCenterX() // Number label containerView.addSubview(numberLabel) @@ -89,8 +89,12 @@ final class CalendarDateViewCell: UICollectionViewCell { // Indicators containerView.addSubview(stackView) let topPos = containerSize.height * 0.7 - stackView.pinTop(topPos).pinBottom().pinTopGreaterThanOrEqualTo().pinTrailingLessThanOrEqualTo() - stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + stackView + .pinTop(topPos) + .pinBottom() + .pinTopGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .pinCenterX() } /// Updating UI based on selected date, modified indicators data along with surface. @@ -102,12 +106,12 @@ final class CalendarDateViewCell: UICollectionViewCell { numberLabel.text = text // enable/disable cells based on min date, max date and active/inactive dates. - self.updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) + updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) // handling inactive dates. if inactiveDates.count > 0 { - for x in (0...(inactiveDates.count-1)) { - if (inactiveDates[x].monthInt == displayDate.monthInt) && (inactiveDates[x].yearInt == displayDate.yearInt) { + for x in 0...inactiveDates.count-1 { + if inactiveDates[x].monthInt == displayDate.monthInt && inactiveDates[x].yearInt == displayDate.yearInt { if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { disableLabel(with: surface) } @@ -116,10 +120,15 @@ final class CalendarDateViewCell: UICollectionViewCell { } // update text color, bg color, corner radius. - if (numberLabel.text == self.getDay(with: selectedDate)) && (selectedDate.monthInt == displayDate.monthInt) && (selectedDate.yearInt == displayDate.yearInt) && numberLabel.isEnabled { + if numberLabel.text == selectedDate.getDay() + && selectedDate.monthInt == displayDate.monthInt + && selectedDate.yearInt == displayDate.yearInt + && numberLabel.isEnabled { + numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor layer.cornerRadius = VDSFormControls.borderRadius + } else { numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface) layer.backgroundColor = nil @@ -128,19 +137,18 @@ final class CalendarDateViewCell: UICollectionViewCell { // add indicators. if indicatorCount > 0 { - for x in (0...(indicators.count-1)) { - // irrespective of month and year, if it needs to show indicators on every month - comment below first if condition, else uncomment it. - // if (indicators[x].date.monthInt == displayDate.monthInt) && (indicators[x].date.yearInt == displayDate.yearInt) { - if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { - let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) - addIndicator(with: color, surface: surface, clearFullCircle: (x == 1), drawSemiCircle: (x == 2)) - } - // } + for x in 0...indicators.count-1 { + if numberLabel.text == indicators[x].date.getDay() { + let color = numberLabel.text == selectedDate.getDay() ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) + addIndicator(with: color, surface: surface, clearFullCircle: x == 1, drawSemiCircle: x == 2) + } } } // update text style for current date. - if (numberLabel.text == self.getDay(with: currentDate)) && (currentDate.monthInt == displayDate.monthInt) && (currentDate.yearInt == displayDate.yearInt) { + if numberLabel.text == currentDate.getDay() + && currentDate.monthInt == displayDate.monthInt + && currentDate.yearInt == displayDate.yearInt { numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall @@ -159,8 +167,8 @@ final class CalendarDateViewCell: UICollectionViewCell { } func showActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { - for x in (0...(activeDates.count-1)) { - if (activeDates[x].monthInt == displayDate.monthInt) && (activeDates[x].yearInt == displayDate.yearInt ) { + for x in 0...activeDates.count-1 { + if activeDates[x].monthInt == displayDate.monthInt && activeDates[x].yearInt == displayDate.yearInt { if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { numberLabel.isEnabled = true } @@ -189,7 +197,7 @@ final class CalendarDateViewCell: UICollectionViewCell { func minDateValidation(with surface:Surface, minDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { // validate days to enable/disable with min date only. - if let day:Int = Int(numberLabel.text), day < minDate.dayInt { + if let day = Int(numberLabel.text), day < minDate.dayInt { disableLabel(with: surface) } else { numberLabel.isEnabled = false @@ -199,7 +207,7 @@ final class CalendarDateViewCell: UICollectionViewCell { func maxDateValidation(with surface:Surface, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { // validate days to enable/disable with max date only. - if let day:Int = Int(numberLabel.text), day > maxDate.dayInt { + if let day = Int(numberLabel.text), day > maxDate.dayInt { disableLabel(with: surface) } else { numberLabel.isEnabled = false @@ -209,7 +217,7 @@ final class CalendarDateViewCell: UICollectionViewCell { func minAndMaxDateValidation(with surface:Surface, minDate: Date, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { // validate days to enable/disable with min and max date. - if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { + if let day = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { disableLabel(with: surface) } else { numberLabel.isEnabled = false @@ -220,33 +228,33 @@ final class CalendarDateViewCell: UICollectionViewCell { // enable/disable cells based on min date, max date and active/inactive dates. func updateLabel(with surface: Surface, displayDate: Date, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { - if (minDate.yearInt == displayDate.yearInt) && !(maxDate.yearInt == displayDate.yearInt) { + if minDate.yearInt == displayDate.yearInt && !(maxDate.yearInt == displayDate.yearInt) { // min year and max year are different, and matched to min year. - if (minDate.monthInt == displayDate.monthInt) { + if minDate.monthInt == displayDate.monthInt { // min year and max year are different, and matched to min year and min month. minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } - } else if (maxDate.yearInt == displayDate.yearInt) && !((minDate.yearInt == displayDate.yearInt)) { + } else if maxDate.yearInt == displayDate.yearInt && !(minDate.yearInt == displayDate.yearInt) { // min year and max year are different, and matched to max year. - if (maxDate.monthInt == displayDate.monthInt) { + if maxDate.monthInt == displayDate.monthInt { // min year and max year are different, and matched to max year and max month. maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } - } else if (minDate.yearInt == displayDate.yearInt) && (maxDate.yearInt == displayDate.yearInt) { + } else if minDate.yearInt == displayDate.yearInt && maxDate.yearInt == displayDate.yearInt { // min year and max year same - if (minDate.monthInt == displayDate.monthInt) && (maxDate.monthInt == displayDate.monthInt) { + if minDate.monthInt == displayDate.monthInt && maxDate.monthInt == displayDate.monthInt { // min year and max year same, when choose dates in same month. minAndMaxDateValidation(with: surface, minDate: minDate, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) - } else if (minDate.monthInt == displayDate.monthInt) || (maxDate.monthInt == displayDate.monthInt) { + } else if minDate.monthInt == displayDate.monthInt || maxDate.monthInt == displayDate.monthInt { // min year and max year same, and choose dates in different months. - if (minDate.monthInt == displayDate.monthInt) { + if minDate.monthInt == displayDate.monthInt { // min year and max year same, and matched to min month. minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else if (maxDate.monthInt == displayDate.monthInt) { @@ -272,14 +280,21 @@ final class CalendarDateViewCell: UICollectionViewCell { $0.backgroundColor = .clear $0.layer.borderWidth = 1.0 } - indicatorView.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + + indicatorView + .pinLeading() + .pinTrailing() + .width(8) + .height(8) + .pinCenterY() + stackView.addArrangedSubview(indicatorView) // update indicator indicatorView.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) indicatorView.layer.borderColor = color.cgColor - self.layoutIfNeeded() + layoutIfNeeded() indicatorView.layer.cornerRadius = indicatorView.frame.size.height / 2.0 @@ -296,16 +311,4 @@ final class CalendarDateViewCell: UICollectionViewCell { guard indicatorView.layer.sublayers?.contains(shapeLayer) ?? true else { return } indicatorView.layer.addSublayer(shapeLayer) } - - func getDay(with date: Date) -> String { - if #available(iOS 15.0, *) { - return date.formatted(.dateTime.day()) - } else { - // Fallback on earlier versions - let dateFormatter: DateFormatter = DateFormatter() - dateFormatter.dateFormat = "d" - let day: String = dateFormatter.string(from: date) - return day - } - } } diff --git a/VDS/Components/Calendar/CalendarFooterReusableView.swift b/VDS/Components/Calendar/CalendarFooterReusableView.swift index 3e85a6ec..f8432bda 100644 --- a/VDS/Components/Calendar/CalendarFooterReusableView.swift +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -65,7 +65,6 @@ class CalendarFooterReusableView: UICollectionReusableView { addSubview(containerView) containerView -// .pinTop(VDSLayout.space6X) .pinTopLessThanOrEqualTo(topAnchor, VDSLayout.space6X, .defaultLow) .pinBottom() .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space3X, .defaultHigh) @@ -99,7 +98,11 @@ extension CalendarFooterReusableView: UICollectionViewDelegate, UICollectionView let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LegendCollectionViewCell.identifier, for: indexPath) as? LegendCollectionViewCell, indexPath.row <= items.count else { return UICollectionViewCell() } let text = items[indexPath.row].label - cell.updateTitle(text: text, color: VDSColor.elementsSecondaryOnlight, surface: self.surface, clearFullcircle: indexPath.row == 1, drawSemiCircle: indexPath.row == 2) + cell.updateTitle(text: text, + color: VDSColor.elementsSecondaryOnlight, + surface: self.surface, + clearFullcircle: indexPath.row == 1, + drawSemiCircle: indexPath.row == 2) return cell } } diff --git a/VDS/Components/Calendar/CalendarHeaderReusableView.swift b/VDS/Components/Calendar/CalendarHeaderReusableView.swift index b1d18810..d0f7f247 100644 --- a/VDS/Components/Calendar/CalendarHeaderReusableView.swift +++ b/VDS/Components/Calendar/CalendarHeaderReusableView.swift @@ -153,7 +153,7 @@ class CalendarHeaderReusableView: UICollectionReusableView { /// Updating UI based on next/previous clicks along with surface. /// Updating UI to enable/disable the next & previous buttons, updating header title. - func update(with surface: Surface, aDate: Date, nextEnabled: Bool, previousEnabled: Bool) { + func update(with surface: Surface, date: Date, nextEnabled: Bool, previousEnabled: Bool) { self.surface = surface headerTitle.surface = surface previousButton.surface = surface @@ -161,7 +161,7 @@ class CalendarHeaderReusableView: UICollectionReusableView { nextButton.isEnabled = nextEnabled previousButton.isEnabled = previousEnabled daysCollectionView.reloadData() - let labelText = aDate.getMonthName(date: aDate) + " \(aDate.yearInt)" + let labelText = date.getMonthName() + " \(date.yearInt)" headerTitle.text = labelText headerTitle.textColor = headerTitleTextColorConfiguration.getColor(surface) } diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index ca4f5a3b..25d8c9de 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -22,18 +22,6 @@ public extension Date { } } - /// Returns all month names - static var fullMonthNames: [String] { - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale.current - - return (1...12).compactMap { month in - dateFormatter.setLocalizedDateFormatFromTemplate("MMMM") - let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1)) - return date.map { dateFormatter.string(from: $0) } - } - } - var startOfMonth: Date { Calendar.current.dateInterval(of: .month, for: self)!.start } @@ -45,7 +33,7 @@ public extension Date { /// Get the number of days of the month var numberOfDaysInMonth: Int { - Calendar.current.component(.day, from: endOfMonth) + Calendar.current.range(of: .day, in: .month, for: self)!.count } var firstWeekDayBeforeStart: Date { @@ -98,9 +86,22 @@ public extension Date { } /// Returns the month name of the given date - func getMonthName(date: Date) -> String { - let names = Date.fullMonthNames - return names[date.monthInt - 1] + func getMonthName() -> String { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.setLocalizedDateFormatFromTemplate("MMMM") + return dateFormatter.string(from: self) + } + + func getDay() -> String { + if #available(iOS 15.0, *) { + return formatted(.dateTime.day()) + } else { + // Fallback on earlier versions + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "d" + let day: String = dateFormatter.string(from: self) + return day + } } - } From 6d0e334f73bb1cd6665ec33b7ae86c929b09ab3f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 16:41:02 -0500 Subject: [PATCH 80/87] refactored out classes/structs into files Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 8 + VDS/Components/Calendar/Calendar.swift | 11 +- VDS/Components/DatePicker/DatePicker.swift | 169 +----------------- .../DatePicker/DatePickerCalendarModel.swift | 62 +++++++ .../DatePicker/DatePickerViewController.swift | 71 ++++++++ 5 files changed, 150 insertions(+), 171 deletions(-) create mode 100644 VDS/Components/DatePicker/DatePickerCalendarModel.swift create mode 100644 VDS/Components/DatePicker/DatePickerViewController.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 10329d45..b1a2ac68 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -159,6 +159,8 @@ EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; EAC58C252BF2A7FB00BA39FA /* DatePickerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */; }; + EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; }; + EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -373,6 +375,8 @@ EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = ""; }; EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = ""; }; + EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = ""; }; + EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -947,6 +951,8 @@ isa = PBXGroup; children = ( EAC58C222BF2824200BA39FA /* DatePicker.swift */, + EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */, + EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */, EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */, ); path = DatePicker; @@ -1281,6 +1287,7 @@ EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */, EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */, + EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */, EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, @@ -1327,6 +1334,7 @@ EA3361B6288B2A410071C351 /* Control.swift in Sources */, EAC58C122BED0DDD00BA39FA /* Text.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, + EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */, EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 18465c47..1b1a797c 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -12,7 +12,7 @@ import Combine /// A calendar is a monthly view that lets customers select a single date. @objc(VDSCalendar) -open class CalendarBase: View { +open class CalendarBase: Control, Changeable { //-------------------------------------------------- // MARK: - Initializers @@ -32,6 +32,8 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- + open var onChangeSubscriber: AnyCancellable? + /// If set to true, the calendar will not have a border. open var hideContainerBorder: Bool = false { didSet { setNeedsUpdate() } } @@ -61,10 +63,7 @@ open class CalendarBase: View { /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. open var indicators: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } - - /// A callback when the date changes. Passes parameters (selectedDate). - public var onChangeSelectedDate: ((Date) -> Void)? - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -326,7 +325,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI // Callback to pass selected date if it is enabled only. selectedDate = dates[indexPath.row] - onChangeSelectedDate?(selectedDate) + sendActions(for: .valueChanged) displayDate = selectedDate var reloadIndexPaths = [indexPath] diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 710de91a..ff9fbb06 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -5,7 +5,7 @@ import Combine /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. @objc(VDSDatePicker) -open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, UIPopoverPresentationControllerDelegate { +open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -52,9 +52,7 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, open var selectedDate: Date? { didSet { setNeedsUpdate() } } - open var calendarModel: CalendarModel = .init() { - didSet { setNeedsUpdate() } - } + open var calendarModel: CalendarModel = .init() { didSet { setNeedsUpdate() } } open override var value: String? { get { selectedDateLabel.text } @@ -158,8 +156,7 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, } internal func togglePicker() { - //let calendarVC = DatePickerPopoverViewController(calendar: Calendar(identifier: .gregorian), delegate: self) - let calendarVC = DatePickerPopoverViewController(calendarModel, delegate: self) + let calendarVC = DatePickerViewController(calendarModel, delegate: self) calendarVC.modalPresentationStyle = .popover calendarVC.selectedDate = selectedDate ?? Date() if let popoverController = calendarVC.popoverPresentationController { @@ -173,7 +170,7 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, } } - internal func didSelectDate(_ controller: DatePickerPopoverViewController, date: Date) { + internal func didSelectDate(_ controller: DatePickerViewController, date: Date) { selectedDate = date controller.dismiss(animated: true) sendActions(for: .valueChanged) @@ -183,161 +180,3 @@ open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, return .none } } - -extension DatePicker { - public struct CalendarModel { - public let surface: Surface - - /// If set to true, the calendar will not have a border. - public let hideContainerBorder: Bool - - /// If set to true, the calendar will not have current date indication. - public let hideCurrentDateIndicator: Bool - - /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. - /// All other dates will be inactive. - public let activeDates: [Date] - - /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. - /// All other dates will be active. - public let inactiveDates: [Date] - - /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. - public let minDate: Date - - /// If provided, the calendar will allow a selection to be made up to this date. - public let maxDate: Date - - /// If provided, this is the date that will show as selected by the Calendar. - /// If no value is provided, the current date will be used. If null is provided, no date will be selected. - public let selectedDate: Date - - /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. - public let indicators: [CalendarBase.CalendarIndicatorModel] - - public init(surface: Surface = .light, - hideContainerBorder: Bool = false, - hideCurrentDateIndicator: Bool = false, - selectedDate: Date = Date(), - activeDates: [Date] = [], - inactiveDates: [Date] = [], - minDate: Date = Date().startOfMonth, - maxDate: Date = Date().endOfMonth, - indicators: [CalendarBase.CalendarIndicatorModel] = []) { - self.surface = surface - self.hideContainerBorder = hideContainerBorder - self.hideCurrentDateIndicator = hideCurrentDateIndicator - self.selectedDate = selectedDate - self.activeDates = activeDates - self.inactiveDates = inactiveDates - self.minDate = minDate - self.maxDate = maxDate - self.indicators = indicators - } - } -} - -protocol DatePickerPopoverViewControllerDelegate: NSObject { - func didSelectDate(_ controller: DatePicker.DatePickerPopoverViewController, date: Date) -} - -//class DatePickerPopoverViewController: UIViewController { -// -// private let picker = UIDatePicker() -// weak var delegate: DatePickerPopoverViewControllerDelegate? -// -// init(calendar: Calendar, delegate: DatePickerPopoverViewControllerDelegate?) { -// self.delegate = delegate -// super.init(nibName: nil, bundle: nil) -// picker.datePickerMode = .date -// picker.preferredDatePickerStyle = .inline -// picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) -// } -// -// var selectedDate: Date = Date() { -// didSet { -// picker.date = selectedDate -// } -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// override func viewDidLoad() { -// super.viewDidLoad() -// let v = UIView().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.backgroundColor = .white -// $0.width(constant: 250) -// $0.height(constant: 350) -// } -// view.addSubview(v) -// v.pinTop(25).pinLeading(15).pinTrailing(15).pinBottom(15) -// -// view.backgroundColor = .blue -// -// preferredContentSize = CGSize(width: 300, height: 400) // Adjust as needed -// } -// -// @objc private func dateChanged(_ sender: UIDatePicker) { -// delegate?.didSelectDate(self, date: sender.date) -// } -//} -extension DatePicker { - class DatePickerPopoverViewController: UIViewController { - private var padding: CGFloat = 15 - private var topPadding: CGFloat { 10 + padding } - private var calendarModel: CalendarModel - private let picker = CalendarBase() - weak var delegate: DatePickerPopoverViewControllerDelegate? - - init(_ calendarModel: CalendarModel, delegate: DatePickerPopoverViewControllerDelegate?) { - self.delegate = delegate - self.calendarModel = calendarModel - super.init(nibName: nil, bundle: nil) - self.picker.onChangeSelectedDate = { [weak self] date in - guard let self else { return } - self.delegate?.didSelectDate(self, date: date) - } - } - - var selectedDate: Date = Date() { - didSet { - picker.selectedDate = selectedDate - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - view.addSubview(picker) - picker.surface = calendarModel.surface - picker.hideContainerBorder = calendarModel.hideContainerBorder - picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator - picker.activeDates = calendarModel.activeDates - picker.inactiveDates = calendarModel.inactiveDates - picker.selectedDate = calendarModel.selectedDate - picker.indicators = calendarModel.indicators - picker.minDate = calendarModel.minDate - picker.maxDate = calendarModel.maxDate - picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) - view.backgroundColor = picker.backgroundColor - } - - override var preferredContentSize: CGSize { - get { - var size = picker.containerSize - size.height += 40 - size.width += 30 - return size - } - set { - super.preferredContentSize = newValue - } - } - } -} diff --git a/VDS/Components/DatePicker/DatePickerCalendarModel.swift b/VDS/Components/DatePicker/DatePickerCalendarModel.swift new file mode 100644 index 00000000..b9b8c8f7 --- /dev/null +++ b/VDS/Components/DatePicker/DatePickerCalendarModel.swift @@ -0,0 +1,62 @@ +// +// DatePicker-CalendarModel.swift +// VDS +// +// Created by Matt Bruce on 5/14/24. +// + +import Foundation +import UIKit + +extension DatePicker { + public struct CalendarModel { + public let surface: Surface + + /// If set to true, the calendar will not have a border. + public let hideContainerBorder: Bool + + /// If set to true, the calendar will not have current date indication. + public let hideCurrentDateIndicator: Bool + + /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be inactive. + public let activeDates: [Date] + + /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be active. + public let inactiveDates: [Date] + + /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. + public let minDate: Date + + /// If provided, the calendar will allow a selection to be made up to this date. + public let maxDate: Date + + /// If provided, this is the date that will show as selected by the Calendar. + /// If no value is provided, the current date will be used. If null is provided, no date will be selected. + public let selectedDate: Date + + /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. + public let indicators: [CalendarBase.CalendarIndicatorModel] + + public init(surface: Surface = .light, + hideContainerBorder: Bool = false, + hideCurrentDateIndicator: Bool = false, + selectedDate: Date = Date(), + activeDates: [Date] = [], + inactiveDates: [Date] = [], + minDate: Date = Date().startOfMonth, + maxDate: Date = Date().endOfMonth, + indicators: [CalendarBase.CalendarIndicatorModel] = []) { + self.surface = surface + self.hideContainerBorder = hideContainerBorder + self.hideCurrentDateIndicator = hideCurrentDateIndicator + self.selectedDate = selectedDate + self.activeDates = activeDates + self.inactiveDates = inactiveDates + self.minDate = minDate + self.maxDate = maxDate + self.indicators = indicators + } + } +} diff --git a/VDS/Components/DatePicker/DatePickerViewController.swift b/VDS/Components/DatePicker/DatePickerViewController.swift new file mode 100644 index 00000000..165dab75 --- /dev/null +++ b/VDS/Components/DatePicker/DatePickerViewController.swift @@ -0,0 +1,71 @@ +// +// DatePickerPopoverViewController.swift +// VDS +// +// Created by Matt Bruce on 5/14/24. +// + +import Foundation +import UIKit + +protocol DatePickerViewControllerDelegate: NSObject { + func didSelectDate(_ controller: DatePicker.DatePickerViewController, date: Date) +} + +extension DatePicker { + class DatePickerViewController: UIViewController { + private var padding: CGFloat = 15 + private var topPadding: CGFloat { 10 + padding } + private var calendarModel: CalendarModel + private let picker = CalendarBase() + weak var delegate: DatePickerViewControllerDelegate? + + init(_ calendarModel: CalendarModel, delegate: DatePickerViewControllerDelegate?) { + self.delegate = delegate + self.calendarModel = calendarModel + super.init(nibName: nil, bundle: nil) + self.picker.onChange = { [weak self] control in + guard let self else { return } + self.delegate?.didSelectDate(self, date: control.selectedDate) + } + } + + var selectedDate: Date = Date() { + didSet { + picker.selectedDate = selectedDate + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(picker) + picker.surface = calendarModel.surface + picker.hideContainerBorder = calendarModel.hideContainerBorder + picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator + picker.indicators = calendarModel.indicators + picker.activeDates = calendarModel.activeDates + picker.inactiveDates = calendarModel.inactiveDates + picker.selectedDate = calendarModel.selectedDate + picker.minDate = calendarModel.minDate + picker.maxDate = calendarModel.maxDate + picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) + view.backgroundColor = picker.backgroundColor + } + + override var preferredContentSize: CGSize { + get { + var size = picker.frame.size + size.height += 40 + size.width += 30 + return size + } + set { + super.preferredContentSize = newValue + } + } + } +} From 85adb2be69249029744c3913e304b2eaf5dfad8f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 16:43:49 -0500 Subject: [PATCH 81/87] fixed bug for seelcted date Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePickerCalendarModel.swift | 6 ------ VDS/Components/DatePicker/DatePickerViewController.swift | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/VDS/Components/DatePicker/DatePickerCalendarModel.swift b/VDS/Components/DatePicker/DatePickerCalendarModel.swift index b9b8c8f7..ccceb5c9 100644 --- a/VDS/Components/DatePicker/DatePickerCalendarModel.swift +++ b/VDS/Components/DatePicker/DatePickerCalendarModel.swift @@ -32,17 +32,12 @@ extension DatePicker { /// If provided, the calendar will allow a selection to be made up to this date. public let maxDate: Date - /// If provided, this is the date that will show as selected by the Calendar. - /// If no value is provided, the current date will be used. If null is provided, no date will be selected. - public let selectedDate: Date - /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. public let indicators: [CalendarBase.CalendarIndicatorModel] public init(surface: Surface = .light, hideContainerBorder: Bool = false, hideCurrentDateIndicator: Bool = false, - selectedDate: Date = Date(), activeDates: [Date] = [], inactiveDates: [Date] = [], minDate: Date = Date().startOfMonth, @@ -51,7 +46,6 @@ extension DatePicker { self.surface = surface self.hideContainerBorder = hideContainerBorder self.hideCurrentDateIndicator = hideCurrentDateIndicator - self.selectedDate = selectedDate self.activeDates = activeDates self.inactiveDates = inactiveDates self.minDate = minDate diff --git a/VDS/Components/DatePicker/DatePickerViewController.swift b/VDS/Components/DatePicker/DatePickerViewController.swift index 165dab75..f8a6e2f0 100644 --- a/VDS/Components/DatePicker/DatePickerViewController.swift +++ b/VDS/Components/DatePicker/DatePickerViewController.swift @@ -49,7 +49,7 @@ extension DatePicker { picker.indicators = calendarModel.indicators picker.activeDates = calendarModel.activeDates picker.inactiveDates = calendarModel.inactiveDates - picker.selectedDate = calendarModel.selectedDate + picker.selectedDate = selectedDate picker.minDate = calendarModel.minDate picker.maxDate = calendarModel.maxDate picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) From a031ea8077df1c47b93c3745e7ebf76168fc78e7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 17:25:50 -0500 Subject: [PATCH 82/87] CXTDT-557216 - BadgeIndicator - Accessibility Signed-off-by: Matt Bruce --- VDS/Components/BadgeIndicator/BadgeIndicator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/BadgeIndicator/BadgeIndicator.swift b/VDS/Components/BadgeIndicator/BadgeIndicator.swift index aae927a8..463e5df8 100644 --- a/VDS/Components/BadgeIndicator/BadgeIndicator.swift +++ b/VDS/Components/BadgeIndicator/BadgeIndicator.swift @@ -350,7 +350,7 @@ open class BadgeIndicator: View { open override func updateAccessibility() { super.updateAccessibility() if let accessibilityText { - accessibilityLabel = accessibilityText + accessibilityLabel = kind == .numbered ? label.text + " " + accessibilityText : accessibilityText } else if kind == .numbered { accessibilityLabel = label.text } else { From 6826b0798d9baf74265a1c2c23debc0a19b5cd78 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 17:42:22 -0500 Subject: [PATCH 83/87] updated release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 39447d3e..461efee9 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,7 +1,12 @@ 1.0.63 ---------------- - CXTDT-555860 - Dropdown Select - Form Elements layout option missing - +- ONEAPP-7963 - InputField - Finished Development +- ONEAPP-7958 - Calendar - Finished Development +- ONEAPP-7010 - DatePicker - Finished Development +- CXTDT-555860 - Dropdown Select - Form Elements layout option missing +- CXTDT-557216 - BadgeIndicator - Accessibility +- CXTDT-555854 - Dropdown Select - spacing issues 1.0.62 ---------------- From 70d94a1a48e0dde97ecd3998ee9d43820cab0e26 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 14 May 2024 17:45:04 -0500 Subject: [PATCH 84/87] updated version Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index b1a2ac68..cca7c7f7 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -1509,7 +1509,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 63; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1546,7 +1546,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 63; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; From ff0117dc1da8fc0d4818a8f593a952c75e9aab57 Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 16 May 2024 16:55:00 +0530 Subject: [PATCH 85/87] Hide legend view when date indicators are empty --- VDS/Components/Calendar/Calendar.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index f7c1940c..fd3e3452 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -67,7 +67,7 @@ open class CalendarBase: Control, Changeable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: 328, height: 376) } + internal var containerSize: CGSize { CGSize(width: 328, height: 336) } internal var calendar = Calendar.current private let cellItemSize = CGSize(width: 40, height: 40) @@ -358,7 +358,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - return CGSize(width: collectionView.frame.size.width, height: footerHeight) + return CGSize(width: collectionView.frame.size.width, height: indicators.count > 0 ? footerHeight : 0) } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { From 9c1b12810233d8918ad4a90545e9c35a26b74776 Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 16 May 2024 17:06:01 +0530 Subject: [PATCH 86/87] Showing 3 variations of icons for date indicators for legend --- .../Calendar/CalendarFooterReusableView.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/VDS/Components/Calendar/CalendarFooterReusableView.swift b/VDS/Components/Calendar/CalendarFooterReusableView.swift index f8432bda..1a402dc3 100644 --- a/VDS/Components/Calendar/CalendarFooterReusableView.swift +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -40,7 +40,9 @@ class CalendarFooterReusableView: UICollectionReusableView { $0.backgroundColor = .clear $0.delegate = self $0.dataSource = self - $0.register(LegendCollectionViewCell.self, forCellWithReuseIdentifier: LegendCollectionViewCell.identifier) + + $0.register(LegendCollectionViewCell.self, + forCellWithReuseIdentifier: LegendCollectionViewCell.identifier) } //-------------------------------------------------- @@ -160,7 +162,11 @@ private class LegendCollectionViewCell: UICollectionViewCell { func setupCell() { addSubview(stackView) stackView.pinToSuperView() - + } + + func updateView() { + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + legendIndicator.layer.sublayers?.forEach { $0.removeFromSuperlayer() } legendIndicatorWrapper.addSubview(legendIndicator) legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() @@ -169,6 +175,7 @@ private class LegendCollectionViewCell: UICollectionViewCell { } func updateTitle(text: String, color: UIColor, surface: Surface, clearFullcircle: Bool, drawSemiCircle: Bool) { + updateView() title.surface = surface title.text = text title.textColor = textColorConfiguration.getColor(surface) From 44f3e7120881fb2f7df271dac70790686ac63f5c Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 17 May 2024 15:27:27 +0530 Subject: [PATCH 87/87] legend alignment set to left and handling label overflow --- .../Calendar/CalendarFooterReusableView.swift | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/VDS/Components/Calendar/CalendarFooterReusableView.swift b/VDS/Components/Calendar/CalendarFooterReusableView.swift index 1a402dc3..9547084f 100644 --- a/VDS/Components/Calendar/CalendarFooterReusableView.swift +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -20,17 +20,26 @@ class CalendarFooterReusableView: UICollectionReusableView { private var surface: Surface = .light private var items: [CalendarBase.CalendarIndicatorModel] = [] internal var containerSize: CGSize { CGSize(width: 304, height: 40) } - + internal var indicatorWidth = 8.0 + + var textLabel: Label = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .left + $0.textStyle = .bodySmall + $0.numberOfLines = 1 + } + internal var containerView = View().with { $0.clipsToBounds = true } - private let flowLayout = UICollectionViewFlowLayout().with { + private let flowLayout = LeftAlignedCollectionViewFlowLayout().with { $0.estimatedItemSize = UICollectionViewFlowLayout.automaticSize $0.minimumLineSpacing = VDSLayout.space1X $0.minimumInteritemSpacing = VDSLayout.space4X $0.scrollDirection = .vertical } + open lazy var legendCollectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout).with { $0.isScrollEnabled = false $0.translatesAutoresizingMaskIntoConstraints = false @@ -45,6 +54,8 @@ class CalendarFooterReusableView: UICollectionReusableView { forCellWithReuseIdentifier: LegendCollectionViewCell.identifier) } + private var topConstraint: NSLayoutConstraint? + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -66,18 +77,17 @@ class CalendarFooterReusableView: UICollectionReusableView { isAccessibilityElement = false addSubview(containerView) - containerView - .pinTopLessThanOrEqualTo(topAnchor, VDSLayout.space6X, .defaultLow) - .pinBottom() - .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space3X, .defaultHigh) - .pinTrailingLessThanOrEqualTo(trailingAnchor, VDSLayout.space3X, .defaultHigh) - .width(containerSize.width - (2*VDSLayout.space3X)) - .heightLessThanEqualTo(containerSize.height) + containerView.pinToSuperView() // legend Collection View containerView.addSubview(legendCollectionView) - legendCollectionView.pinTop().pinBottom().pinLeading().pinTrailing().pinCenterY().pinCenterX() - + legendCollectionView + .pinTopLessThanOrEqualTo(topAnchor, VDSLayout.space6X, .defaultLow) + .pinBottom() + .pinLeading(VDSLayout.space3X) + .pinTrailing(VDSLayout.space3X) + .width(containerSize.width - (2 * VDSLayout.space3X)) + .heightGreaterThanEqualTo(16) } /// Updating UI to show legend with titles. @@ -85,6 +95,15 @@ class CalendarFooterReusableView: UICollectionReusableView { self.items = indicators self.surface = surface legendCollectionView.reloadData() + + var height = legendCollectionView.collectionViewLayout.collectionViewContentSize.height + if height > 0 { + topConstraint?.isActive = false + height = height > containerSize.height ? containerSize.height : height + let top = containerSize.height - height + topConstraint = legendCollectionView.topAnchor.constraint(equalTo: topAnchor, constant: top) + topConstraint?.isActive = true + } } } @@ -107,6 +126,14 @@ extension CalendarFooterReusableView: UICollectionViewDelegate, UICollectionView drawSemiCircle: indexPath.row == 2) return cell } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + textLabel.text = items[indexPath.row].label + let intrinsicSize = textLabel.intrinsicContentSize + let cellwidth = intrinsicSize.width + indicatorWidth + VDSLayout.space2X + return .init(width: min(cellwidth, collectionView.frame.width), height: intrinsicSize.height) + } + } private class LegendCollectionViewCell: UICollectionViewCell { @@ -130,6 +157,7 @@ private class LegendCollectionViewCell: UICollectionViewCell { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear } + private var legendIndicator: View = View().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear @@ -138,7 +166,8 @@ private class LegendCollectionViewCell: UICollectionViewCell { private lazy var stackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false - $0.distribution = .equalSpacing + $0.alignment = .fill + $0.distribution = .fill $0.spacing = VDSLayout.space2X $0.axis = .horizontal $0.backgroundColor = .clear @@ -146,6 +175,8 @@ private class LegendCollectionViewCell: UICollectionViewCell { private lazy var shapeLayer = CAShapeLayer() + internal var indicatorSize: CGSize { CGSize(width: 8, height: 8) } + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -167,8 +198,9 @@ private class LegendCollectionViewCell: UICollectionViewCell { func updateView() { stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } legendIndicator.layer.sublayers?.forEach { $0.removeFromSuperlayer() } + legendIndicatorWrapper.addSubview(legendIndicator) - legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + legendIndicator.pinLeading().pinTrailing().width(indicatorSize.width).height(indicatorSize.height).pinCenterY() stackView.addArrangedSubview(legendIndicatorWrapper) stackView.addArrangedSubview(title) @@ -201,3 +233,25 @@ private class LegendCollectionViewCell: UICollectionViewCell { legendIndicator.layer.addSublayer(shapeLayer) } } + +class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var maxY: CGFloat = -1.0 + attributes?.forEach { layoutAttribute in + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + + layoutAttribute.frame.origin.x = leftMargin + + leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing + maxY = max(layoutAttribute.frame.maxY , maxY) + } + + return attributes + } +}