Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios.git into refactor/VDSTokens

# Conflicts:
#	VDS.xcodeproj/project.pbxproj
#	VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift
#	VDS/Components/Notification/Notification.swift
#	VDS/Components/TextFields/EntryFieldBase.swift
#	VDS/Components/TileContainer/TileContainer.swift
#	VDS/Extensions/VDSLayout.swift

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-04-02 12:09:30 -05:00
commit 5d19edbc6e
74 changed files with 3506 additions and 426 deletions

View File

@ -7,16 +7,35 @@
objects = {
/* Begin PBXBuildFile section */
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; };
18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */; };
1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; };
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; };
1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */; };
186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; };
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; };
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.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 */; };
5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; };
5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; };
710607952B91A99500F2863F /* TitleletChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 710607942B91A99500F2863F /* TitleletChangeLog.txt */; };
7115BD3C2B84C0C200E0A610 /* TileContainerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */; };
71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */; };
71ACE89C2BA0451200FB6ADC /* PaginationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */; };
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */; };
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71B23C2C2B91FA690027F7D9 /* Pagination.swift */; };
71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */; };
71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */; };
71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */; };
71FC86DA2B96F44C00700965 /* PaginationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86D92B96F44C00700965 /* PaginationButton.swift */; };
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */; };
71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */; };
71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */; };
71FC86E22B97483000700965 /* Clamping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86E12B97483000700965 /* Clamping.swift */; };
71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */; };
EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; };
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; };
EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; };
@ -67,6 +86,7 @@
EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; };
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; };
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */; };
EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6F330D2B911E9000BACAB9 /* TextView.swift */; };
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; };
EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; };
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; };
@ -94,6 +114,8 @@
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEB828ECD24B003B3210 /* Icons.xcassets */; };
EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */; };
EAA7456C2AB23E2000C1841F /* TooltipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA7456B2AB23E2000C1841F /* TooltipModel.swift */; };
EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAACB8972B92706F006A3869 /* DefaultValuing.swift */; };
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAACB8992B927108006A3869 /* Valuing.swift */; };
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */; };
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CC28ABE76000DAE764 /* Withable.swift */; };
EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CE28ABEF2B00DAE764 /* Typography+Base.swift */; };
@ -124,6 +146,7 @@
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068912A560B65002E3A2D /* LoaderViewController.swift */; };
EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */; };
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */; };
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE785302BA0A438009428EA /* UIImage+Helper.swift */; };
EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */; };
EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */; };
EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */; };
@ -169,16 +192,35 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = "<group>"; };
18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = "<group>"; };
1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = "<group>"; };
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = "<group>"; };
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = "<group>"; };
186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = "<group>"; };
18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = "<group>"; };
18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = "<group>"; };
18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = "<group>"; };
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = "<group>"; };
44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = "<group>"; };
5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
710607942B91A99500F2863F /* TitleletChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TitleletChangeLog.txt; sourceTree = "<group>"; };
7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = "<group>"; };
71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dropshadowable.swift; sourceTree = "<group>"; };
71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationContainer.swift; sourceTree = "<group>"; };
71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiletEyebrowModel.swift; sourceTree = "<group>"; };
71B23C2C2B91FA690027F7D9 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = "<group>"; };
71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PaginationChangeLog.txt; sourceTree = "<group>"; };
71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowable.swift; sourceTree = "<group>"; };
71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = NotificationChangeLog.txt; sourceTree = "<group>"; };
71FC86D92B96F44C00700965 /* PaginationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationButton.swift; sourceTree = "<group>"; };
71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationCellItem.swift; sourceTree = "<group>"; };
71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceConfigurationValue.swift; sourceTree = "<group>"; };
71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowConfiguration.swift; sourceTree = "<group>"; };
71FC86E12B97483000700965 /* Clamping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clamping.swift; sourceTree = "<group>"; };
71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationFlowLayout.swift; sourceTree = "<group>"; };
EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = "<group>"; };
EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = "<group>"; };
EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = "<group>"; };
@ -230,6 +272,7 @@
EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = "<group>"; };
EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = "<group>"; };
EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = "<group>"; };
EA6F330D2B911E9000BACAB9 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = "<group>"; };
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = "<group>"; };
@ -258,6 +301,8 @@
EAA5EEDF28F49DB3003B3210 /* Colorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colorable.swift; sourceTree = "<group>"; };
EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGDS-Light.otf"; sourceTree = "<group>"; };
EAA7456B2AB23E2000C1841F /* TooltipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipModel.swift; sourceTree = "<group>"; };
EAACB8972B92706F006A3869 /* DefaultValuing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultValuing.swift; sourceTree = "<group>"; };
EAACB8992B927108006A3869 /* Valuing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valuing.swift; sourceTree = "<group>"; };
EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = "<group>"; };
EAB1D2CC28ABE76000DAE764 /* Withable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Withable.swift; sourceTree = "<group>"; };
EAB1D2CE28ABEF2B00DAE764 /* Typography+Base.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typography+Base.swift"; sourceTree = "<group>"; };
@ -287,6 +332,7 @@
EAD068912A560B65002E3A2D /* LoaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderViewController.swift; sourceTree = "<group>"; };
EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderLaunchable.swift; sourceTree = "<group>"; };
EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = "<group>"; };
EAE785302BA0A438009428EA /* UIImage+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Helper.swift"; sourceTree = "<group>"; };
EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BadgeChangeLog.txt; sourceTree = "<group>"; };
EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonChangeLog.txt; sourceTree = "<group>"; };
EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LineChangeLog.txt; sourceTree = "<group>"; };
@ -342,6 +388,27 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
18A65A002B96E7E1006602CC /* Breadcrumbs */ = {
isa = PBXGroup;
children = (
18A65A012B96E848006602CC /* Breadcrumbs.swift */,
18A65A032B96F050006602CC /* BreadcrumbItem.swift */,
1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */,
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */,
18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */,
);
path = Breadcrumbs;
sourceTree = "<group>";
};
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */ = {
isa = PBXGroup;
children = (
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */,
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */,
);
path = CarouselScrollbar;
sourceTree = "<group>";
};
445BA07629C07ABA0036A7C5 /* Notification */ = {
isa = PBXGroup;
children = (
@ -370,6 +437,19 @@
path = Button;
sourceTree = "<group>";
};
71B23C2B2B91FA510027F7D9 /* Pagination */ = {
isa = PBXGroup;
children = (
71B23C2C2B91FA690027F7D9 /* Pagination.swift */,
71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */,
71FC86D92B96F44C00700965 /* PaginationButton.swift */,
71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */,
71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */,
71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */,
);
path = Pagination;
sourceTree = "<group>";
};
EA0B17FF2A9E21CA00F2D0CD /* Selector */ = {
isa = PBXGroup;
children = (
@ -473,13 +553,16 @@
children = (
EA4DB2FE28DCBC1900103EE3 /* Badge */,
EAD062AE2A3B87210015965D /* BadgeIndicator */,
18A65A002B96E7E1006602CC /* Breadcrumbs */,
EA0FC2BE2912D18200DF80B4 /* Buttons */,
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
EAF7F092289985E200B287F5 /* Checkbox */,
EA985BF3296C609E00F2FF2E /* Icon */,
EA3362412892EF700071C351 /* Label */,
44604AD529CE195300E62B51 /* Line */,
EAD0688C2A55F801002E3A2D /* Loader */,
445BA07629C07ABA0036A7C5 /* Notification */,
71B23C2B2B91FA510027F7D9 /* Pagination */,
EA89200B28B530F0006B9984 /* RadioBox */,
EAF7F11428A1470D00B287F5 /* RadioButton */,
EA596ABB2A16B4D500300C4B /* Tabs */,
@ -516,6 +599,7 @@
EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */,
EA33623D2892EE950071C351 /* UIDevice.swift */,
EA0B18092AA78F9000F2D0CD /* UIEdgeInsets.swift */,
EAE785302BA0A438009428EA /* UIImage+Helper.swift */,
EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */,
EA8E40902A7D3F6300934ED3 /* UIView+Accessibility.swift */,
EAB5FED329267EB300998C17 /* UIView+NSLayoutConstraint.swift */,
@ -533,6 +617,7 @@
EAF1FE9A29DB1A6000101452 /* Changeable.swift */,
EAF1FE9829D4850E00101452 /* Clickable.swift */,
EAA5EEDF28F49DB3003B3210 /* Colorable.swift */,
EAACB8972B92706F006A3869 /* DefaultValuing.swift */,
EA3361A9288B25E40071C351 /* Disabling.swift */,
EAF978202A99035B00C2FEA9 /* Enabling.swift */,
EA5E305929510F8B0082B959 /* EnumSubset.swift */,
@ -546,7 +631,8 @@
EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */,
EAB1D2CC28ABE76000DAE764 /* Withable.swift */,
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */,
71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */,
EAACB8992B927108006A3869 /* Valuing.swift */,
71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */,
);
path = Protocols;
sourceTree = "<group>";
@ -565,6 +651,9 @@
isa = PBXGroup;
children = (
EA3361BC288B2C760071C351 /* TypeAlias.swift */,
71FC86E12B97483000700965 /* Clamping.swift */,
71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */,
71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -659,9 +748,11 @@
children = (
EA5E3057295105A40082B959 /* Tilelet.swift */,
EA985BE529688F6A00F2FF2E /* TileletBadgeModel.swift */,
EA985BE929689B6D00F2FF2E /* TileletSubTitleModel.swift */,
71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */,
EA985BE72968951C00F2FF2E /* TileletTitleModel.swift */,
EA985BE929689B6D00F2FF2E /* TileletSubTitleModel.swift */,
EA985C2C296F03FE00F2FF2E /* TileletIconModels.swift */,
710607942B91A99500F2863F /* TitleletChangeLog.txt */,
);
path = Tilelet;
sourceTree = "<group>";
@ -701,6 +792,8 @@
isa = PBXGroup;
children = (
EA985C22296E033A00F2FF2E /* TextArea.swift */,
EA6F330D2B911E9000BACAB9 /* TextView.swift */,
186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */,
);
path = TextArea;
sourceTree = "<group>";
@ -932,23 +1025,28 @@
EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */,
71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */,
EAEEECA72B1F952000531FC2 /* TabsChangeLog.txt in Resources */,
186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */,
EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */,
710607952B91A99500F2863F /* TitleletChangeLog.txt in Resources */,
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */,
EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */,
EAEEECA22B1F92AD00531FC2 /* LabelChangeLog.txt in Resources */,
EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */,
EAEEEC9A2B1F8E4400531FC2 /* TextLinkChangeLog.txt in Resources */,
1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */,
EAEEECAF2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt in Resources */,
EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */,
EAEEEC9E2B1F8F7700531FC2 /* ButtonGroupChangeLog.txt in Resources */,
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */,
EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */,
EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */,
18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */,
EAEEECA02B1F908200531FC2 /* BadgeIndicatorChangeLog.txt in Resources */,
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */,
EAEEECA92B1F969700531FC2 /* TooltipChangeLog.txt in Resources */,
EAEEEC9C2B1F8F0700531FC2 /* TextLinkCaretChangeLog.txt in Resources */,
EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */,
71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */,
EAEEECAD2B1FC1A600531FC2 /* TitleLockupChangeLog.txt in Resources */,
EAEEECAB2B1FBF2A00531FC2 /* ToggleChangeLog.txt in Resources */,
);
@ -973,21 +1071,28 @@
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */,
EA0D1C3F2A6AD5E200E5C127 /* Typography+ContentSizeCategory.swift in Sources */,
EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */,
EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */,
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */,
EA3361C328902D960071C351 /* Toggle.swift in Sources */,
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */,
EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */,
71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */,
EAC9258C2911C9DE00091998 /* InputField.swift in Sources */,
EA3362402892EF6C0071C351 /* Label.swift in Sources */,
EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */,
EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */,
EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */,
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */,
EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */,
71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */,
71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */,
EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */,
EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */,
71FC86E22B97483000700965 /* Clamping.swift in Sources */,
EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */,
1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */,
EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */,
EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */,
EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */,
@ -997,18 +1102,24 @@
EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */,
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */,
EAC925842911C63100091998 /* Colorable.swift in Sources */,
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */,
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */,
EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */,
EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */,
71ACE89C2BA0451200FB6ADC /* PaginationContainer.swift in Sources */,
EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */,
EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */,
EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */,
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */,
EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */,
EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */,
EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */,
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */,
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */,
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */,
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */,
EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */,
71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */,
EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */,
EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */,
EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */,
@ -1022,6 +1133,7 @@
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
71FC86DA2B96F44C00700965 /* PaginationButton.swift in Sources */,
EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */,
EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */,
EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */,
@ -1029,6 +1141,7 @@
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
@ -1040,6 +1153,7 @@
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */,
EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */,
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */,
EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */,
EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */,
@ -1048,6 +1162,7 @@
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */,
EA513A952A4E1F82002A4DFF /* TitleLockupStyleConfiguration.swift in Sources */,
44604AD729CE196600E62B51 /* Line.swift in Sources */,
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */,
EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */,
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */,
EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */,
@ -1078,6 +1193,7 @@
EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */,
EA33623E2892EE950071C351 /* UIDevice.swift in Sources */,
EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */,
71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */,
EA3362302891EB4A0071C351 /* Font.swift in Sources */,
EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */,
EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */,
@ -1237,7 +1353,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
@ -1274,7 +1390,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -132,4 +132,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
return true
}
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsUpdate()
}
}

View File

@ -101,7 +101,6 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
/// Instead of use labelText and labelTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes.
open var labelAttributedText: NSAttributedString? {
didSet {
label.useAttributedText = !(labelAttributedText?.string.isEmpty ?? true)
label.attributedText = labelAttributedText
setNeedsUpdate()
}
@ -116,7 +115,6 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
/// Instead of use childText and childTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes.
open var childAttributedText: NSAttributedString? {
didSet {
childLabel.useAttributedText = !(childAttributedText?.string.isEmpty ?? true)
childLabel.attributedText = childAttributedText
setNeedsUpdate()
}

View File

@ -86,4 +86,9 @@ open class View: UIView, ViewProtocol, UserInfoable {
isEnabled = true
}
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsUpdate()
}
}

View File

@ -34,10 +34,17 @@ public final class SelfSizingCollectionView: UICollectionView {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var contentSizeObservation: NSKeyValueObservation?
private var collectionViewHeight: NSLayoutConstraint?
private var anyCancellable: AnyCancellable?
private var contentSizeSubject = CurrentValueSubject<CGSize, Never>(.zero)
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var contentSizePublisher: AnyPublisher<CGSize, Never> {
contentSizeSubject.eraseToAnyPublisher()
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
@ -45,7 +52,6 @@ public final class SelfSizingCollectionView: UICollectionView {
/// The natural size for the receiving view, considering only properties of the view itself.
public override var intrinsicContentSize: CGSize {
let contentSize = self.contentSize
//print(#function, contentSize)
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
@ -67,18 +73,17 @@ public final class SelfSizingCollectionView: UICollectionView {
//ensure autoLayout uses intrinsic height
setContentHuggingPriority(.required, for: .vertical)
setContentCompressionResistancePriority(.required, for: .vertical)
collectionViewHeight = heightAnchor.constraint(equalToConstant: 0).activate()
// Observing the value of contentSize seems to be the only reliable way to get the contentSize after the collection view lays out its subviews.
self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in
// If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`.
if change.newValue != change.oldValue {
self?.invalidateIntrinsicContentSize()
if let height = change.newValue?.height {
self?.collectionViewHeight?.constant = height
collectionViewHeight = height(constant: 0, priority: .defaultHigh)
anyCancellable = self.publisher(for: \.contentSize, options: [.new])
.sink { [weak self] compare in
guard let self else { return }
if compare.height != self.collectionViewHeight?.constant {
self.invalidateIntrinsicContentSize()
self.collectionViewHeight?.constant = compare.height
self.contentSizeSubject.send(compare)
}
}
}
}
}

View File

@ -146,6 +146,7 @@ open class Badge: View {
label.widthGreaterThanEqualTo(constant: minWidth)
maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false }
clipsToBounds = true
}
/// Resets to default settings.

View File

@ -0,0 +1,90 @@
//
// BreadcrumbCellItem.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 11/03/24.
//
import UIKit
import VDSTokens
///This is customised view for Breadcrumb cell item
final class BreadcrumbCellItem: UICollectionViewCell {
///Identifier for the BreadcrumbCellItem
static let identifier: String = String(describing: BreadcrumbCellItem.self)
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal var stackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.alignment = .fill
$0.spacing = VDSLayout.space1X
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
}
}()
internal var breadCrumbItem: BreadcrumbItem?
///separator label
private var separator: Label = Label().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.textAlignment = .left
$0.numberOfLines = 1
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
$0.text = "/"
}
private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUp()
}
///Configuring the cell with default setup
private func setUp() {
separator.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
contentView.addSubview(stackView)
stackView.pinToSuperView()
separator.backgroundColor = .clear
}
///Updating the breadCrumbItem and UI based on the selected flag along with the surface
func update(surface: Surface, hideSlash: Bool, breadCrumbItem: BreadcrumbItem) {
//update surface
separator.surface = surface
breadCrumbItem.surface = surface
breadCrumbItem.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
breadCrumbItem.setContentHuggingPriority(.defaultLow, for: .horizontal)
//remove previous views
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
//add to stack
stackView.addArrangedSubview(separator)
stackView.addArrangedSubview(breadCrumbItem)
stackView.setCustomSpacing(VDSLayout.space1X, after: separator)
//update separator
separator.textColor = textColorConfiguration.getColor(surface)
separator.isHidden = hideSlash
self.breadCrumbItem = breadCrumbItem
layoutIfNeeded()
}
}

View File

@ -0,0 +1,102 @@
//
// BreadcrumbItem.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 05/03/24.
//
import Foundation
import UIKit
import VDSTokens
import Combine
/// A Breadcrumb Item contains href(link) and selected flag.
/// Breadcrumb links to its respective page if it is not disabled.
/// Breadcrumb contains text with a separator by default, highlights text in bold without a separator if selected.
@objc (VDSBreadcrumbItem)
open class BreadcrumbItem: ButtonBase {
//--------------------------------------------------
// 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
//--------------------------------------------------
/// TextStyle used on the titleLabel.
open override var textStyle: TextStyle { isSelected ? TextStyle.boldBodySmall : TextStyle.bodySmall }
/// UIColor used on the titleLabel text.
open override var textColor: UIColor {
textColorConfiguration.getColor(self)
}
/// The natural size for the receiving view, considering only properties of the view itself.
open override var intrinsicContentSize: CGSize {
guard let titleLabel else { return super.intrinsicContentSize }
// Calculate the titleLabel's intrinsic content size
let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width, height: CGFloat.greatestFiniteMagnitude))
// Adjust the size if needed (add any additional padding if your design requires)
let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right,
height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
return adjustedSize
}
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var textColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
}
//--------------------------------------------------
// 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()
isAccessibilityElement = true
accessibilityTraits = .link
contentHorizontalAlignment = .leading
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
//always call last so the label is rendered
super.updateView()
}
/// Resets to default settings.
open override func reset() {
super.reset()
shouldUpdateView = false
text = nil
accessibilityCustomActions = []
isAccessibilityElement = true
accessibilityTraits = .button
shouldUpdateView = true
setNeedsUpdate()
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = "Breadcrumb \(text ?? "")"
}
}

View File

@ -0,0 +1,32 @@
//
// BreadcrumbItemModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 20/03/24.
//
import Foundation
extension Breadcrumbs {
public struct BreadcrumbItemModel {
///Text that goes in the breadcrumb item
public var text: String
/// Whether the Item can be clicked.
public var enabled: Bool
/// The Breadcrumb link to links to its respective page.
public var selected: Bool
///Click event when you click on a breadcrumb item
public var onClick: ((BreadcrumbItem) -> Void)?
public init(text: String, enabeled: Bool = true, selected: Bool = false, onClick: ((BreadcrumbItem) -> Void)? = nil) {
self.text = text
self.enabled = enabeled
self.selected = selected
self.onClick = onClick
}
}
}

View File

@ -0,0 +1,170 @@
//
// Breadcrumbs.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 11/03/24.
//
import Foundation
import UIKit
import VDSTokens
import Combine
/// A Breadcrumbs contains BreadcrumbItems.
/// It contains Breadcrumb Item Default, Breadcrumb Item Selected, Separator.
/// Breadcrumbs are secondary navigation that use a hierarchy of internal links to tell customers where they are in an experience. Each breadcrumb links to its respective page, except for that of current page.
@objc(VDSBreadcrumbs)
open class Breadcrumbs: View {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Array of ``BreadcrumbItem`` views for the Breadcrumbs.
open var breadcrumbs: [BreadcrumbItem] = [] { didSet { setNeedsUpdate() } }
/// Array of ``BreadcurmbItemModel`` you are wanting to show.
open var breadcrumbModels: [BreadcrumbItemModel] = [] { didSet { updateBreadcrumbItems() } }
/// Whether this object is enabled or not
override open var isEnabled: Bool {
didSet {
breadcrumbs.forEach { $0.isEnabled = isEnabled }
}
}
/// Current Surface and this is used to pass down to child objects that implement Surfacable
override open var surface: Surface {
didSet {
breadcrumbs.forEach { $0.surface = surface }
}
}
/// A callback when the selected item changes. Passes parameters (crumb).
open var onBreadcrumbDidSelect: ((BreadcrumbItem) -> Void)?
/// A callback when the Tab determine if a item should be selected.
open var onBreadcrumbShouldSelect:((BreadcrumbItem) -> Bool)?
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
fileprivate lazy var layout = ButtonGroupPositionLayout().with {
$0.position = .left
$0.delegate = self
$0.axisSpacer = { _, _, _ in
return VDSLayout.space1X
}
$0.verticalSpacer = { _, _ in
return VDSLayout.space1X
}
}
///Collectionview to render Breadcrumb Items
private lazy var collectionView: SelfSizingCollectionView = {
let collectionView = SelfSizingCollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.isScrollEnabled = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
collectionView.register(BreadcrumbCellItem.self, forCellWithReuseIdentifier: BreadcrumbCellItem.identifier)
collectionView.backgroundColor = .clear
return collectionView
}()
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
/// Removes all of the Breadcrumbs and creates new ones from the Breadcrumb Models property.
private func updateBreadcrumbItems() {
// Clear existing breadcrumbs
for breadcrumbItem in breadcrumbs {
breadcrumbItem.removeFromSuperview()
}
// Create new breadcrumb items from the models
breadcrumbs = breadcrumbModels.compactMap({ model in
let breadcrumbItem = BreadcrumbItem()
breadcrumbItem.text = model.text
breadcrumbItem.isEnabled = model.enabled
breadcrumbItem.isSelected = model.selected
breadcrumbItem.onClick = { [weak self] breadcrumb in
guard let self, breadcrumb.isEnabled else { return }
if self.onBreadcrumbShouldSelect?(breadcrumb) ?? true {
model.onClick?(breadcrumb)
self.onBreadcrumbDidSelect?(breadcrumb)
}
}
return breadcrumbItem
})
}
//------------------------------------------s--------
// MARK: - Overrides
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
addSubview(collectionView)
collectionView.pinToSuperView()
}
/// Resets to default settings.
open override func reset() {
super.reset()
shouldUpdateView = false
breadcrumbs.forEach { $0.reset() }
shouldUpdateView = true
setNeedsUpdate()
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
collectionView.reloadData()
}
open override func layoutSubviews() {
//Turn off the ability to execute updateView() in the super
//since we don't want an infinite loop
shouldUpdateView = false
super.layoutSubviews()
shouldUpdateView = true
// Accounts for any collection size changes
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
private var separatorWidth = Label().with { $0.text = "/"; $0.sizeToFit() }.intrinsicContentSize.width
}
extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource, ButtongGroupPositionLayoutDelegate {
//--------------------------------------------------
// MARK: - UICollectionView Delegate & Datasource
//--------------------------------------------------
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
breadcrumbs.count
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BreadcrumbCellItem.identifier, for: indexPath) as? BreadcrumbCellItem else { return UICollectionViewCell() }
let hideSlash = indexPath.row == 0
cell.update(surface: surface, hideSlash: hideSlash, breadCrumbItem: breadcrumbs[indexPath.row])
return cell
}
public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
let breadcrumb = breadcrumbs[indexPath.row]
let intrinsicSize = breadcrumb.intrinsicContentSize
let separatorFullWidth: CGFloat = indexPath.row == 0 ? 0 : VDSLayout.space1X + separatorWidth
let cellwidth = intrinsicSize.width + separatorFullWidth
return .init(width: min(cellwidth, collectionView.frame.width), height: intrinsicSize.height)
}
public func collectionView(_ collectionView: UICollectionView, buttonBaseAtIndexPath indexPath: IndexPath) -> ButtonBase {
breadcrumbs[indexPath.row]
}
}

View File

@ -0,0 +1,36 @@
MM/DD/YYYY
----------------
- Initial Brand 3.0 handoff
12/17/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
2/28/2022
----------------
- Changed Last Breadcrumb Item to Selected Item
03/08/2022
----------------
- Added dev note for Active and Hover states.
08/04/2022
----------------
- Updated default and inverted prop to light and dark surface.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced focus border pixel and style & spacing values with tokens.
01/03/2022
----------------
- Updated Specs to use new SPEC Templates and SPEC DOC Components.
01/06/2023
----------------
- Tweaked anatomy element naming to align with design doc and dev doc

View File

@ -96,11 +96,15 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
/// Whether the Control is enabled or not.
open override var isEnabled: Bool { didSet { setNeedsUpdate() } }
/// Whether the Control is selected or not.
open override var isSelected: Bool { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open func initialSetup() {
if !initialSetupPerformed {
initialSetupPerformed = true
backgroundColor = .clear
translatesAutoresizingMaskIntoConstraints = false
accessibilityCustomActions = []
@ -139,22 +143,13 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
shouldUpdateView = true
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
override open var intrinsicContentSize: CGSize {
let intrinsicContentSize = super.intrinsicContentSize
let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right
let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom
return CGSize(width: adjustedWidth, height: adjustedHeight)
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateLabel() {
defer { invalidateIntrinsicContentSize() }
//clear the arrays holding actions
accessibilityCustomActions = []
if let text, !text.isEmpty {

View File

@ -98,6 +98,8 @@ open class ButtonGroup: View {
buttons.forEach { $0.surface = surface }
}
}
open var contentSizePublisher: AnyPublisher<CGSize, Never> { collectionView.contentSizePublisher }
//--------------------------------------------------
// MARK: - Private Properties
@ -108,6 +110,7 @@ open class ButtonGroup: View {
$0.delegate = self
}
/// CollectionView that renders the array of buttonBase obects.
fileprivate lazy var collectionView: SelfSizingCollectionView = {
return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with {

View File

@ -6,21 +6,18 @@
//
import Foundation
import UIKit
struct ButtonGroupConstants {
static let defaultSpace = 12.0
enum ButtonSpacingAxis {
case horizontal, vertical
}
/// This will determine the spacing that will go between 2 ButtonBases either horizontally or vertically
/// - Parameters:
/// - axis: horizontal/vertical
/// - primary: first ButtonBase
/// - neighboring: next ButtonBase based off of axis
/// - Returns: float value
static func getSpacing(for axis: ButtonSpacingAxis, with primary: ButtonBase, neighboring: ButtonBase) -> CGFloat {
static func getSpacing(for axis: NSLayoutConstraint.Axis, with primary: ButtonBase, neighboring: ButtonBase) -> CGFloat {
//large button
if let button = primary as? Button, button.size == .large {

View File

@ -146,6 +146,8 @@ class ButtonLayoutAttributes: UICollectionViewLayoutAttributes{
class ButtonGroupPositionLayout: UICollectionViewLayout {
weak var delegate: ButtongGroupPositionLayoutDelegate?
var verticalSpacer: ((ButtonCollectionViewRow, ButtonCollectionViewRow?) -> CGFloat)?
var axisSpacer: ((NSLayoutConstraint.Axis, ButtonBase, ButtonBase) -> CGFloat)?
// Total height of the content. Will be used to configure the scrollview content
var layoutHeight: CGFloat = 0.0
@ -154,7 +156,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
var buttonPercentage: CGFloat?
private var itemCache: [ButtonLayoutAttributes] = []
override func prepare() {
super.prepare()
@ -226,7 +228,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
let neighbor = delegate.collectionView(collectionView, buttonBaseAtIndexPath: IndexPath(item: nextItem, section: section))
// get the spacing to go between the current and next button
itemSpacing = ButtonGroupConstants.getSpacing(for: .horizontal, with: itemButtonBase, neighboring: neighbor)
itemSpacing = getAxisSpacing(for: .horizontal, with: itemButtonBase, neighboring: neighbor)
}
// create the custom layout attribute
@ -255,7 +257,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
if item > 0 {
let prevRow = rows[item - 1]
rowSpacing = ButtonGroupConstants.getVerticalSpacing(for: prevRow, neighboringRow: row)
rowSpacing = getVerticalSpacing(for: prevRow, neighboringRow: row)
row.rowY = layoutHeight + rowSpacing
layoutHeight += rowSpacing
}
@ -300,5 +302,19 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
}
return collectionView.bounds.width
}
private func getAxisSpacing(for axis: NSLayoutConstraint.Axis, with primary: ButtonBase, neighboring: ButtonBase) -> CGFloat {
guard let axisSpacer else {
return ButtonGroupConstants.getSpacing(for: axis, with: primary, neighboring: neighboring)
}
return axisSpacer(axis, primary, neighboring)
}
private func getVerticalSpacing(for row: ButtonCollectionViewRow, neighboringRow: ButtonCollectionViewRow?) -> CGFloat {
guard let verticalSpacer else {
return ButtonGroupConstants.getVerticalSpacing(for: row, neighboringRow: neighboringRow)
}
return verticalSpacer(row, neighboringRow)
}
}

View File

@ -74,7 +74,13 @@ open class TextLink: ButtonBase {
/// The natural size for the receiving view, considering only properties of the view itself.
open override var intrinsicContentSize: CGSize {
return titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize
guard let titleLabel else { return super.intrinsicContentSize }
// Calculate the titleLabel's intrinsic content size
let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width, height: CGFloat.greatestFiniteMagnitude))
// Adjust the size if needed (add any additional padding if your design requires)
let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right,
height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
return adjustedSize
}
//--------------------------------------------------
@ -86,6 +92,10 @@ open class TextLink: ButtonBase {
isAccessibilityElement = true
accessibilityTraits = .link
//left align titleLabel in case this is pinned leading/trailing
//default is always set to center
contentHorizontalAlignment = .left
if let titleLabel {
addSubview(line)
line.pinLeading(titleLabel.leadingAnchor)

View File

@ -78,6 +78,11 @@ open class TextLinkCaret: ButtonBase {
/// 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()
//left align titleLabel in case this is pinned leading/trailing
//default is always set to center
contentHorizontalAlignment = .left
accessibilityTraits = .link
titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping
@ -97,10 +102,23 @@ open class TextLinkCaret: ButtonBase {
}
/// The natural size for the receiving view, considering only properties of the view itself.
override open var intrinsicContentSize: CGSize {
//get the labels size, if not the button
return titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize
open override var intrinsicContentSize: CGSize {
guard let titleLabel else { return super.intrinsicContentSize }
// Calculate the titleLabel's intrinsic content size
let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width - (contentEdgeInsets.left + contentEdgeInsets.right), height: CGFloat.greatestFiniteMagnitude))
// Adjust the size if needed (add any additional padding if your design requires)
let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right,
height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
return adjustedSize
}
open override func layoutSubviews() {
super.layoutSubviews()
// This ensures the titleLabel is correctly positioned within the button
titleLabel?.preferredMaxLayoutWidth = self.frame.width - (contentEdgeInsets.left + contentEdgeInsets.right)
super.layoutSubviews() // Calling super again to ensure layout is updated with preferredMaxLayoutWidth
}
}
extension TextLinkCaret {

View File

@ -0,0 +1,435 @@
//
// CarouselScrollbar.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 12/03/24.
//
import Foundation
import UIKit
import VDSTokens
import Combine
/// A carousel scrollbar is a control that allows to navigate between items in a carousel.
/// It's also a status indicator that conveys the relative amount of content in a carousel and a location within it.
@objc(VDSCarouselScrollbar)
open class CarouselScrollbar: 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
//--------------------------------------------------
/// Used to set total number of slides within carousel
open var numberOfSlides: Int {
get { return _numberOfSlides }
set {
_numberOfSlides = newValue
setThumbWidth()
scrollThumbToPosition(position)
setNeedsUpdate()
}
}
/// The number of slides that can appear at once in a set in a carousel container.
open var selectedLayout: Layout? {
get { return _selectedLayout }
set {
if let newValue {
_selectedLayout = newValue
} else {
_selectedLayout = .oneUP
}
setThumbWidth()
scrollThumbToPosition(position)
setNeedsUpdate()
}
}
/// Enum used to describe the number of slides that can appear at once in a set in a carousel container.
public enum Layout: String, CaseIterable {
case oneUP = "1UP"
case twoUP = "2UP"
case threeUP = "3UP"
case fourUP = "4UP"
case fiveUP = "5UP"
case sixUP = "6UP"
case eightUP = "8UP"
var value: Int {
switch self {
case .oneUP:
1
case .twoUP:
2
case .threeUP:
3
case .fourUP:
4
case .fiveUP:
5
case .sixUP:
6
case .eightUP:
8
}
}
}
/// Used to set the position of the thumb(scrubber). This is used when the carousel container changes position, it will align the position of thumb(scrubber).
open var position: Int {
get { return _position }
set {
checkPositions()
_position = (newValue > totalPositions) ? totalPositions : newValue
scrollThumbToPosition(position)
setNeedsUpdate()
}
}
/// Allows a unique id to be passed into the thumb and track of the thumb(scrubber).
open var scrubberId: Int? { didSet { setNeedsUpdate() } }
/// A callback when the scrubber position changes. Passes parameters (position).
open var onScrubberDrag: ((Int) -> Void)? {
get { nil }
set {
onScrubberDragCancellable?.cancel()
if let newValue {
onScrubberDragCancellable = onScrubberDragPublisher
.sink { c in
newValue(c)
}
}
}
}
/// A publisher for when the scrubber position changes. Passes parameters (position).
open var onScrubberDragPublisher = PassthroughSubject<Int, Never>()
private var onScrubberDragCancellable: AnyCancellable?
/// A callback when the thumb move forward. Passes parameters (position).
open var onMoveForward: ((Int) -> Void)? {
get { nil }
set {
onMoveForwardCancellable?.cancel()
if let newValue {
onMoveForwardCancellable = onMoveForwardPublisher
.sink { c in
newValue(c)
}
}
}
}
/// A publisher for when the thumb move forward. Passes parameters (position).
open var onMoveForwardPublisher = PassthroughSubject<Int, Never>()
private var onMoveForwardCancellable: AnyCancellable?
/// A callback when the thumb move backward. Passes parameters (position).
open var onMoveBackward: ((Int) -> Void)? {
get { nil }
set {
onMoveBackwardCancellable?.cancel()
if let newValue {
onMoveBackwardCancellable = onMoveBackwardPublisher
.sink { c in
newValue(c)
}
}
}
}
/// A publisher for when the thumb move backward. Passes parameters (position).
open var onMoveBackwardPublisher = PassthroughSubject<Int, Never>()
private var onMoveBackwardCancellable: AnyCancellable?
/// A callback when the thumb touch start. Passes parameters (position).
open var onThumbTouchStart: ((Int) -> Void)? {
get { nil }
set {
onThumbTouchStartCancellable?.cancel()
if let newValue {
onThumbTouchStartCancellable = onThumbTouchStartPublisher
.sink { c in
newValue(c)
}
}
}
}
/// A publisher for when the thumb touch start. Passes parameters (position).
open var onThumbTouchStartPublisher = PassthroughSubject<Int, Never>()
private var onThumbTouchStartCancellable: AnyCancellable?
/// A callback when the thumb touch end. Passes parameters (position).
open var onThumbTouchEnd: ((Int) -> Void)? {
get { nil }
set {
onThumbTouchEndCancellable?.cancel()
if let newValue {
onThumbTouchEndCancellable = onThumbTouchEndPublisher
.sink { c in
newValue(c)
}
}
}
}
/// A publisher for when the thumb touch end. Passes parameters (position).
open var onThumbTouchEndPublisher = PassthroughSubject<Int, Never>()
private var onThumbTouchEndCancellable: AnyCancellable?
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
// Sizes are from InVision design specs.
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
internal var _selectedLayout: Layout = .oneUP
internal var _numberOfSlides: Int = 1
internal var totalPositions: Int = 1
internal var _position: Int = 1
internal var trayOriginalCenter: CGPoint!
private let trackViewWidth = 96
private let trackViewHeight: CGFloat = 4
private let minThumbWidth: Float = 16.0
private var thumbWidth: Float = 16.0
private var computedWidth: Float = 0.0
private let cornerRadius: CGFloat = 2.0
private let activeOpacity: Float = 0.15
private let defaultOpacity: Float = 1
internal var containerView = View().with {
$0.clipsToBounds = true
}
internal var trackView = View()
internal var leftActiveOverlay = View()
internal var rightActiveOverlay = View()
internal var thumbView = View()
internal var rightActiveOverlayLayer: CALayer = CALayer()
internal var leftActiveOverlayLayer: CALayer = CALayer()
internal var thumbViewLayer: CALayer = CALayer()
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
private var thumbColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveScrollthumbOnlight , VDSColor.interactiveScrollthumbOndark)
private var trackColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveScrolltrackOnlight , VDSColor.interactiveScrolltrackOndark)
private var activeOverlayColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func initialSetup() {
super.initialSetup()
}
open override func setup() {
super.setup()
isAccessibilityElement = true
accessibilityLabel = "Carousel Scrollbar"
addSubview(containerView)
containerView
.pinTop()
.pinBottom()
.pinLeadingGreaterThanOrEqualTo()
.pinTrailingLessThanOrEqualTo()
.height(containerSize.height)
.width(CGFloat(trackViewWidth))
containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate()
//Trackview
trackView.frame = CGRectMake(0, 20, CGFloat(trackViewWidth), trackViewHeight)
trackView.layer.cornerRadius = cornerRadius
containerView.addSubview(trackView)
///Left active overlay
leftActiveOverlay.frame = CGRectMake(trackView.frame.origin.x, 0, CGFloat(trackViewWidth), containerSize.height)
leftActiveOverlay.isUserInteractionEnabled = true
leftActiveOverlay.backgroundColor = .clear
let leftPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onLeftViewLongPressRecognizer(_:)))
leftPressRecognizer.minimumPressDuration = 0
leftActiveOverlay.addGestureRecognizer(leftPressRecognizer)
containerView.addSubview(leftActiveOverlay)
leftActiveOverlay.layer.addSublayer(leftActiveOverlayLayer)
leftActiveOverlayLayer.cornerRadius = cornerRadius
leftActiveOverlayLayer.frame = .init(origin: .zero, size: .init(width: leftActiveOverlayLayer.frame.size.width, height: trackViewHeight))
///Right active overlay
rightActiveOverlay.frame = CGRectMake(thumbView.frame.origin.x + thumbView.frame.size.width, 0, CGFloat(trackViewWidth), containerSize.height)
rightActiveOverlay.isUserInteractionEnabled = true
rightActiveOverlay.backgroundColor = .clear
let rightPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onRightViewLongPressRecognizer(_:)))
rightPressRecognizer.minimumPressDuration = 0
rightActiveOverlay.addGestureRecognizer(rightPressRecognizer)
containerView.addSubview(rightActiveOverlay)
rightActiveOverlay.layer.addSublayer(rightActiveOverlayLayer)
rightActiveOverlayLayer.cornerRadius = cornerRadius
rightActiveOverlayLayer.frame = .init(origin: .zero, size: .init(width: rightActiveOverlay.frame.size.width, height: trackViewHeight))
//Thumbview
thumbView.frame = CGRectMake(0, 0, CGFloat(thumbWidth), containerSize.height)
thumbView.backgroundColor = .clear
thumbView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action:(#selector(onScrubberChange(_:)))))
containerView.addSubview(thumbView)
updateActiveOverlays()
thumbViewLayer.cornerRadius = cornerRadius
thumbViewLayer.backgroundColor = thumbColorConfiguration.getColor(surface).cgColor
thumbViewLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: CGFloat(thumbWidth), height: trackViewHeight))
thumbView.layer.addSublayer(thumbViewLayer)
}
open override func updateView() {
super.updateView()
trackView.backgroundColor = trackColorConfiguration.getColor(surface)
thumbViewLayer.backgroundColor = thumbColorConfiguration.getColor(surface).cgColor
}
open override func updateAccessibility() {
super.updateAccessibility()
}
open override func reset() {
for subview in subviews {
for recognizer in subview.gestureRecognizers ?? [] {
subview.removeGestureRecognizer(recognizer)
}
}
super.reset()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
func movePositionBackward() {
position = position - 1
scrollThumbToPosition(position)
onMoveBackwardPublisher.send(position)
}
func movePositionForward() {
position = position + 1
scrollThumbToPosition(position)
onMoveForwardPublisher.send(position)
}
// Compute track width and should maintain minimum thumb width if needed
private func setThumbWidth() {
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_selectedLayout.value)
computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
thumbView.frame.size.width = CGFloat(thumbWidth)
thumbView.frame.origin.x = trackView.frame.origin.x
thumbViewLayer.frame.size.width = thumbView.frame.size.width
checkPositions()
updateActiveOverlays()
}
// Incomplete set moves a shorter distance than the standard increment value.
// Update active overlay frames according to thumb position.
private func updateActiveOverlays() {
// adjusting thumb position if it goes beyond trackView on left/right.
let thumbPosition = thumbView.frame.origin.x + thumbView.frame.size.width
let trackPosition = trackView.frame.origin.x + trackView.frame.size.width
if thumbPosition > trackPosition {
thumbView.frame.origin.x = trackPosition - thumbView.frame.size.width
} else if thumbView.frame.origin.x < trackView.frame.origin.x {
thumbView.frame.origin.x = trackView.frame.origin.x
}
//left active overlay position update
leftActiveOverlay.frame.size.width = thumbView.frame.origin.x - trackView.frame.origin.x + cornerRadius
leftActiveOverlayLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: leftActiveOverlay.frame.size.width, height: trackViewHeight))
//right active overlay position update
rightActiveOverlay.frame.origin.x = thumbView.frame.origin.x + thumbView.frame.size.width - cornerRadius
rightActiveOverlay.frame.size.width = (trackView.frame.origin.x + trackView.frame.size.width) - (thumbView.frame.origin.x + thumbView.frame.size.width) + cornerRadius
rightActiveOverlayLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: rightActiveOverlay.frame.size.width, height: trackViewHeight))
}
private func checkPositions() {
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value)))
}
private func scrollThumbToPosition(_ position: Int) {
setThumb(at: position)
onScrubberDragPublisher.send(position)
}
private func setThumb(at position: Int) {
UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { [weak self] in
guard let self else { return }
self.thumbView.frame.origin.x = CGFloat(Float((position) - 1) * self.computedWidth) + self.trackView.frame.origin.x
self.updateActiveOverlays()
})
}
//--------------------------------------------------
// MARK: - Gesture Methods
//--------------------------------------------------
// Drag scrollbar thumb to move it to the left or right.
// Upon releases of drag, the scrollbar thumb snaps to the closest full position and the scrollbar returns to original size without delay.
@objc func onScrubberChange(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: thumbView)
if sender.state == UIGestureRecognizer.State.began {
trayOriginalCenter = thumbView.center
onThumbTouchStartPublisher.send(position)
} else if sender.state == UIGestureRecognizer.State.changed {
let draggedPositions = Int (ceil (Double(translation.x) / Double(computedWidth)))
setThumb(at: position + draggedPositions)
}
else if sender.state == UIGestureRecognizer.State.cancelled || sender.state == UIGestureRecognizer.State.ended {
let draggedPositions = Int (ceil (Double(translation.x) / Double(computedWidth)))
position = ((position + draggedPositions) < 1) ? 1 : (position + draggedPositions)
if sender.state == UIGestureRecognizer.State.ended {
onThumbTouchEndPublisher.send(position)
}
}
}
// Move the scrollbar thumb to the left while tapping on the left side of the scrubber.
@objc func onLeftViewLongPressRecognizer(_ gesture: UILongPressGestureRecognizer) {
animateOverlay(layer: leftActiveOverlayLayer, with: gesture, onGestureEnd: movePositionBackward)
}
// Move the scrollbar thumb to the right while tapping on the right side of the scrubber.
@objc func onRightViewLongPressRecognizer(_ gesture: UILongPressGestureRecognizer) {
animateOverlay(layer: rightActiveOverlayLayer, with: gesture, onGestureEnd: movePositionForward)
}
private func animateOverlay(layer: CALayer, with gesture: UILongPressGestureRecognizer, onGestureEnd: @escaping(() -> Void)) {
UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { [weak self] in
guard let self else { return }
if gesture.state == .began {
layer.backgroundColor = activeOverlayColorConfiguration.getColor(self).cgColor
layer.opacity = activeOpacity
} else if gesture.state == .cancelled {
layer.backgroundColor = UIColor.clear.cgColor
layer.opacity = defaultOpacity
} else if gesture.state == .ended {
layer.backgroundColor = UIColor.clear.cgColor
layer.opacity = defaultOpacity
onGestureEnd()
}
})
}
}

View File

@ -0,0 +1,28 @@
MM/DD/YYYY
----------------
07/29/22
----------------
- Initial Brand 3.0 handoff
08/10/2022
----------------
- Updated default and inverted prop to light and dark surface.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced focus border pixel and style & spacing values with tokens.
01/09/2023
----------------
- Updated Specs to use new SPEC Templates and SPEC DOC Components.
05/19/2023
----------------
- Changed Carousel Scrubber to Carousel Scrollbar and replaced all instances of Scrubber to Scrollbar.
- Removed KF states and behaviors from States and Behaviors > Interaction Types.

View File

@ -232,19 +232,26 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
}()
}
private struct LowContrastColorFillFloatingConfiguration: Configuration, Dropshadowable {
private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration {
var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .colorFill
var floating: Bool = true
var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable()
}()
var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}()
var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 4
private let dropshadow1Configuration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
$0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.12), CGFloat(0.22))
$0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1))
$0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(10), CGFloat(12))
}
private let dropshadow2Configuration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
$0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.05), CGFloat(0.15))
$0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2))
$0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(4), CGFloat(6))
}
var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] }
}
private struct LowContrastMediaConfiguration: Configuration, Borderable {
@ -260,19 +267,26 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
}()
}
private struct LowContrastMediaFloatingConfiguration: Configuration, Dropshadowable {
private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration {
var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .media
var floating: Bool = true
var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable()
}()
var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}()
var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 4
private let dropshadow1Configuration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
$0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.12), CGFloat(0.22))
$0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1))
$0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(10), CGFloat(12))
}
private let dropshadow2Configuration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
$0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.05), CGFloat(0.15))
$0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2))
$0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(4), CGFloat(6))
}
var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] }
}
private struct HighContrastConfiguration: Configuration {
@ -291,7 +305,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
}()
}
private struct HighContrastFloatingConfiguration: Configuration, Dropshadowable {
private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration {
var kind: Kind = .highContrast
var surfaceType: SurfaceType = .colorFill
var floating: Bool = true
@ -305,12 +319,19 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.eraseToAnyColorable()
}()
var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}()
var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 6
private let dropshadow1Configuration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
$0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.22), CGFloat(0.12))
$0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1))
$0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(12), CGFloat(10))
}
private let dropshadow2Configuration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
$0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.15), CGFloat(0.05))
$0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2))
$0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(6), CGFloat(4))
}
var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] }
}
private var badgeIndicatorDefaultSize: CGSize = .zero
@ -322,7 +343,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
open override func setup() {
super.setup()
isAccessibilityElement = true
accessibilityTraits = .image
accessibilityTraits = .button
accessibilityElements = [badgeIndicator]
//create a layoutGuide for the icon to key off of
@ -452,12 +473,6 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
layer.borderColor = nil
layer.borderWidth = 0
}
if let dropshadowable = currentConfig as? Dropshadowable {
addDropShadow(dropshadowable)
} else {
removeDropShadows()
}
badgeIndicatorCenterXConstraint?.constant = badgeIndicatorOffset.x + badgeIndicatorDefaultSize.width/2
badgeIndicatorCenterYConstraint?.constant = badgeIndicatorOffset.y + badgeIndicatorDefaultSize.height/2
@ -467,6 +482,12 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
if showBadgeIndicator {
updateExpandDirectionalConstraints()
}
if let configurations = (currentConfig as? DropShadowableConfiguration)?.configurations {
addDropShadows(configurations)
} else {
removeDropShadows()
}
}
//--------------------------------------------------

View File

@ -44,7 +44,7 @@ open class Icon: View {
//--------------------------------------------------
/// UIImageView used to render the icon.
open var imageView = UIImageView().with {
$0.isAccessibilityElement = false
$0.isAccessibilityElement = false
$0.translatesAutoresizingMaskIntoConstraints = false
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true
@ -109,8 +109,8 @@ open class Icon: View {
//get the image name
//set the image
if let name, let image = getImage(for: name.rawValue) {
setImage(image: image, imageColor: imageColor)
if let name, let image = UIImage.image(for: name, color: imageColor) {
imageView.image = image
} else {
imageView.image = nil
}
@ -129,17 +129,17 @@ open class Icon: View {
super.updateAccessibility()
accessibilityLabel = name?.rawValue ?? "icon"
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func getImage(for imageName: String) -> UIImage? {
return BundleManager.shared.image(for: imageName)
}
private func setImage(image: UIImage, imageColor: UIColor) {
imageView.image = image.withTintColor(imageColor)
}
}
extension UIImage {
/// UIImage helper for finding images based on the Icon.Name which uses the internal BundleManager.
/// - Parameters:
/// - name: Icon.Name rawRepresentable.
/// - color: Color to Tint the image with
/// - renderingMode: UIImage Rendering mode.
/// - Returns: UIImage for this proecess
public static func image(for iconName: Icon.Name, color: UIColor? = nil, renderingMode: UIImage.RenderingMode = .alwaysOriginal) -> UIImage? {
image(representing: iconName, color: color, renderingMode: renderingMode)
}
}

View File

@ -48,7 +48,6 @@ 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 checkmark = Name(name: "checkmark")
public static let checkmarkAlt = Name(name: "checkmark-alt")
public static let close = Name(name: "close")

View File

@ -56,6 +56,8 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
if(shouldUnderline){
UnderlineLabelAttribute(location: location, length: length).setAttribute(on: attributedString)
}

View File

@ -30,6 +30,7 @@ public struct AnyAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.removeAttribute(key, range: range)
attributedString.addAttribute(key, value: value, range: range)
}

View File

@ -14,6 +14,8 @@ public protocol AttachmentLabelAttributeModel: LabelAttributeModel {
extension AttachmentLabelAttributeModel {
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
do {
let mutableString = NSMutableAttributedString()
let attachment = try getAttachment()

View File

@ -31,6 +31,8 @@ public struct ColorLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
var colorRange = range
if length == 0 && location == 0 {
colorRange = .init(location: location, length: attributedString.length)

View File

@ -29,9 +29,23 @@ extension LabelAttributeModel {
public static func == (lhs: any LabelAttributeModel, rhs: any LabelAttributeModel) -> Bool {
lhs.isEqual(rhs)
}
public func isValidRange(on attributedString: NSMutableAttributedString) -> Bool {
attributedString.isValid(range: range)
}
}
public extension String {
func isValid(range: NSRange) -> Bool {
range.location >= 0 && range.length > 0 && range.location + range.length <= count
}
}
public extension NSAttributedString {
func isValid(range: NSRange) -> Bool {
range.location >= 0 && range.length > 0 && range.location + range.length <= length
}
func createAttributeModels() -> [(any LabelAttributeModel)] {
var attributes: [any VDS.LabelAttributeModel] = []
enumerateAttributes(in: NSMakeRange(0, length)) { attributeMap, range, stop in
@ -57,6 +71,7 @@ public extension NSAttributedString {
extension NSMutableAttributedString {
public func apply(attribute: any LabelAttributeModel) {
guard isValid(range: attribute.range) else { return }
attribute.setAttribute(on: self)
}

View File

@ -24,6 +24,7 @@ public struct StrikeThroughLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range)
}

View File

@ -44,6 +44,7 @@ public struct TextStyleLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: textStyle.font, range: range)
if let textColor {

View File

@ -24,7 +24,9 @@ public class TooltipLabelAttribute: ActionLabelAttributeModel, TooltipLaunchable
public var presenter: UIView?
public func setAttribute(on attributedString: NSMutableAttributedString) {
//update the location
guard isValidRange(on: attributedString) else { return }
//update the location
location = attributedString.string.count - 1
//set the default color off surface for text

View File

@ -52,7 +52,8 @@ public struct UnderlineLabelAttribute: LabelAttributeModel {
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
public func setAttribute(on attributedString: NSMutableAttributedString) {
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.addAttribute(.underlineStyle, value: underlineValue.rawValue, range: range)
if let color = color {
attributedString.addAttribute(.underlineColor, value: color, range: range)

View File

@ -42,6 +42,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private enum TextSetMode {
case text
case attributedText
}
private var textSetMode: TextSetMode = .text
private var initialSetupPerformed = false
private var edgeInsets: UIEdgeInsets { textStyle.edgeInsets }
@ -101,18 +108,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
// MARK: - Public Properties
//--------------------------------------------------
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
/// Determines if the label should use its own attributedText property instead of rendering the attributedText propert
/// based of other local properties, such as textStyle, textColor, surface, etc... The default value is false.
open var useAttributedText: Bool = false
open var shouldUpdateView: Bool = true
/// Will determine if a scaled font should be used for the font.
open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }}
open var surface: Surface = .light { didSet { setNeedsUpdate() }}
/// Array of LabelAttributeModel objects used in rendering the text.
/// Array of LabelAttributeModel objects used in rendering the text.
open var attributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }}
/// TextStyle used on the this label.
@ -128,19 +131,40 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
/// Line break mode for the label, default is set to word wrapping.
open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }}
private var _text: String?
/// Text that will be used in the label.
override open var text: String? {
get { _text }
set {
if _text != newValue || newValue != attributedText?.string {
_text = newValue
useAttributedText = false
attributes?.removeAll()
setNeedsUpdate()
private var _text: String!
override open var text: String! {
didSet {
_text = text
textSetMode = .text
setNeedsUpdate()
}
}
///AttributedText that will be used in the label.
override open var attributedText: NSAttributedString? {
didSet {
textSetMode = .attributedText
setNeedsUpdate()
}
}
override open var font: UIFont! {
didSet {
if let font, initialSetupPerformed {
textStyle = TextStyle.convert(font: font)
}
setNeedsUpdate()
}
}
override open var textColor: UIColor! {
didSet {
if let textColor, initialSetupPerformed {
textColorConfiguration = SurfaceColorConfiguration(textColor, textColor).eraseToAnyColorable()
}
setNeedsUpdate()
}
}
@ -162,13 +186,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
//--------------------------------------------------
open func initialSetup() {
if !initialSetupPerformed {
initialSetupPerformed = true
//register for ContentSizeChanges
NotificationCenter
.Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification)
.sink { [weak self] notification in
self?.setNeedsUpdate()
}.store(in: &subscribers)
backgroundColor = .clear
numberOfLines = 0
lineBreakMode = .byWordWrapping
@ -200,30 +224,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
open func updateView() {
if !useAttributedText {
if let text {
accessibilityCustomActions = []
//create the primary string
let mutableText = NSMutableAttributedString.mutableText(for: text,
textStyle: textStyle,
useScaledFont: useScaledFont,
textColor: textColorConfiguration.getColor(self),
alignment: textAlignment,
lineBreakMode: lineBreakMode)
applyAttributes(mutableText)
//set the attributed text
attributedText = mutableText
//force a drawText
setNeedsDisplay()
setNeedsLayout()
layoutIfNeeded()
}
}
restyleText()
//force a drawText
setNeedsDisplay()
setNeedsLayout()
layoutIfNeeded()
}
open func updateAccessibility() {
@ -269,6 +276,55 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func restyleText() {
if textSetMode == .text {
styleText(_text)
} else {
styleAttributedText(attributedText)
}
}
private func styleText(_ newValue: String!) {
defer { invalidateIntrinsicContentSize() }
guard let newValue, !newValue.isEmpty else {
// We don't need to use attributed text
super.attributedText = nil
super.text = newValue
return
}
accessibilityCustomActions = []
//create the primary string
let mutableText = NSMutableAttributedString.mutableText(for: newValue,
textStyle: textStyle,
useScaledFont: useScaledFont,
textColor: textColorConfiguration.getColor(self),
alignment: textAlignment,
lineBreakMode: lineBreakMode)
applyAttributes(mutableText)
// Set attributed text to match typography
super.attributedText = mutableText
}
private func styleAttributedText(_ newValue: NSAttributedString?) {
defer { invalidateIntrinsicContentSize() }
guard let newValue, !newValue.string.isEmpty else {
// We don't need any additional styling
super.attributedText = newValue
return
}
let mutableText = NSMutableAttributedString(attributedString: newValue)
applyAttributes(mutableText)
// Modify attributed text to match typography
super.attributedText = mutableText
}
private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) {
actions = []
@ -288,7 +344,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
for attribute in attributes {
//see if the attribute is Actionable
if let actionable = attribute as? any ActionLabelAttributeModel{
if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) {
//create a accessibleAction
let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText)
@ -323,7 +379,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
guard let text = text, let attributedText else { return nil }
let actionText = accessibleText ?? NSString(string:text).substring(with: range)
let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text)
// Calculate the frame of the substring
let layoutManager = NSLayoutManager()
@ -366,3 +422,5 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
}
}

View File

@ -80,15 +80,33 @@ open class Loader: View {
super.updateView()
icon.color = iconColorConfiguration.getColor(self)
icon.customSize = size
if isActive {
if isActive && isVisibleOnScreen {
startAnimating()
} else {
stopAnimating()
}
invalidateIntrinsicContentSize()
}
open override func updateAccessibility() {
super.updateAccessibility()
// check to make sure VoiceOver is running
guard UIAccessibility.isVoiceOverRunning, isActive else {
loadingTimer?.invalidate()
loadingTimer = nil
return
}
// Focus VoiceOver on this view
UIAccessibility.post(notification: .layoutChanged, argument: self)
// setup timer for post
loadingTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
guard let self, self.isActive, self.isVisibleOnScreen else { return }
self.accessibilityLabel = "Still Loading"
UIAccessibility.post(notification: .announcement, argument: "Still Loading")
}
}
//--------------------------------------------------
@ -107,23 +125,6 @@ open class Loader: View {
rotation.duration = 0.5
rotation.repeatCount = .infinity
icon.layer.add(rotation, forKey: rotationLayerName)
// check to make sure VoiceOver is running
guard UIAccessibility.isVoiceOverRunning else {
loadingTimer?.invalidate()
loadingTimer = nil
return
}
// Focus VoiceOver on this view
UIAccessibility.post(notification: .layoutChanged, argument: self)
// setup timer for post
loadingTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
guard let self, self.isActive, self.isVisibleOnScreen else { return }
self.accessibilityLabel = "Still Loading"
UIAccessibility.post(notification: .announcement, argument: "Still Loading")
}
}
private func stopAnimating() {

View File

@ -53,11 +53,6 @@ open class Notification: View {
}
}
/// Enum used to describe the orientation of Notification.
public enum Layout: String, CaseIterable {
case vertical, horizontal
}
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
@ -80,19 +75,9 @@ open class Notification: View {
$0.translatesAutoresizingMaskIntoConstraints = false
}
private var labelButtonViewSpacing: CGFloat {
let spacing: CGFloat = UIDevice.isIPad ? 20 : 16
return switch layout {
case .vertical:
0
case .horizontal:
spacing
}
}
private var labelButtonViewSpacing: CGFloat { UIDevice.isIPad ? 20 : 16 }
internal var onCloseSubscriber: AnyCancellable?
private var maxWidthConstraint: NSLayoutConstraint?
private var leadingConstraint: NSLayoutConstraint?
@ -172,19 +157,6 @@ open class Notification: View {
/// Add this attribute determine your type of Notification.
open var style: Style = .info { didSet { setNeedsUpdate()}}
private var _layout: Layout = .vertical
/// Determines the orientation of buttons and text in the Notification.
open var layout: Layout {
set {
if !UIDevice.isIPad, newValue == .horizontal { return }
_layout = newValue
buttonGroup.alignment = _layout == .horizontal ? .center : .left
setNeedsUpdate()
}
get { _layout }
}
//--------------------------------------------------
// MARK: - Configuration
@ -219,18 +191,11 @@ open class Notification: View {
return 288
}
private var maxViewWidth: CGFloat {
return 1232
}
private var labelViewWidthConstraint: NSLayoutConstraint?
private var labelViewBottomConstraint: NSLayoutConstraint?
private var labelViewAndButtonViewConstraint: NSLayoutConstraint?
private var buttonViewTopConstraint: NSLayoutConstraint?
private var typeIconWidthConstraint: NSLayoutConstraint?
private var closeIconWidthConstraint: NSLayoutConstraint?
private var buttonGroupCenterYConstraint: NSLayoutConstraint?
private var buttonGroupBottomConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Overrides
@ -255,21 +220,18 @@ open class Notification: View {
mainStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: minContentHeight),
layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: minViewWidth)
])
maxWidthConstraint = layoutGuide.widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth)
labelButtonView.addSubview(labelsView)
labelsView
.pinTop()
.pinLeading()
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: 1.0)
labelViewWidthConstraint?.activate()
labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: 1.0).activate()
labelViewBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: labelsView.bottomAnchor)
labelButtonView.addSubview(buttonGroup)
buttonGroup
.pinTrailing()
buttonGroupBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: buttonGroup.bottomAnchor)
buttonGroupCenterYConstraint = buttonGroup.centerYAnchor.constraint(equalTo: labelButtonView.centerYAnchor)
labelButtonView.bottomAnchor.constraint(equalTo: buttonGroup.bottomAnchor).activate()
labelViewAndButtonViewConstraint = buttonGroup.topAnchor.constraint(equalTo: labelsView.bottomAnchor, constant: VDSLayout.space3X)
buttonGroup.widthAnchor.constraint(equalTo: labelsView.widthAnchor).activate()
@ -314,7 +276,6 @@ open class Notification: View {
closeButton.size = UIDevice.isIPad ? .medium : .small
closeButton.name = .close
layout = .vertical
hideCloseButton = false
shouldUpdateView = true
@ -338,6 +299,16 @@ open class Notification: View {
layer.cornerRadius = UIScreen.main.bounds.width == bounds.width ? 0 : 4.0
}
///Updating the accessiblity values i.e elements, label, value other items for the component.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup]
typeIcon.accessibilityLabel = style.rawValue
typeIcon.imageView.image?.isAccessibilityElement = false
closeButton.accessibilityTraits = [.button]
closeButton.accessibilityLabel = "Close Notification"
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
@ -383,28 +354,22 @@ open class Notification: View {
secondaryButton.onClick = secondaryButtonModel.onClick
buttons.append(secondaryButton)
}
labelViewWidthConstraint?.deactivate()
if buttons.isEmpty {
buttonGroup.isHidden = true
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor)
buttonGroup.buttons.removeAll()
} else {
labelsView.setCustomSpacing(VDSLayout.space3X, after: subTitleLabel)
buttonGroup.buttons = buttons
buttonGroup.isHidden = false
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: layout == .vertical ? 1.0 : 0.5, constant: layout == .vertical ? 0 : -labelButtonViewSpacing)
}
labelViewWidthConstraint?.activate()
}
private func setConstraints() {
maxWidthConstraint?.constant = maxViewWidth
maxWidthConstraint?.isActive = UIDevice.isIPad
labelViewAndButtonViewConstraint?.isActive = layout == .vertical && !buttonGroup.buttons.isEmpty
labelViewAndButtonViewConstraint?.deactivate()
labelViewBottomConstraint?.deactivate()
labelViewAndButtonViewConstraint?.isActive = !buttonGroup.buttons.isEmpty
labelViewBottomConstraint?.isActive = buttonGroup.buttons.isEmpty
typeIconWidthConstraint?.constant = typeIcon.size.dimensions.width
closeIconWidthConstraint?.constant = closeButton.size.dimensions.width
labelViewBottomConstraint?.isActive = layout == .horizontal || buttonGroup.buttons.isEmpty
buttonGroupCenterYConstraint?.isActive = layout == .horizontal
buttonGroupBottomConstraint?.isActive = layout == .vertical
}
}

View File

@ -0,0 +1,245 @@
//
// Pagination.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 01/03/24.
//
import Foundation
import UIKit
import VDSTokens
import Combine
///Pagination is a control that enables customers to navigate multiple pages of content by selecting either a specific page or the next or previous set of four pages.
@objc(VDSPagination)
open class Pagination: View {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
///Maximum component width
private let maxWidth: CGFloat = 288.0
///Collectionview width anchor
private var collectionViewWidthAnchor: NSLayoutConstraint?
///Collectionview container Center X constraint
private var collectionContainerViewCenterXConstraint: NSLayoutConstraint?
///Selected page index
private var _selectedPageIndex: Int = 0
///Custom flow layout defined for the Pagination
private let flowLayout = PaginationFlowLayout()
///A root view for the pagination
public let containerView: View = View().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
///Collectionview to render pagination indexes
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.isScrollEnabled = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
collectionView.isAccessibilityElement = true
collectionView.register(PaginationCellItem.self, forCellWithReuseIdentifier: PaginationCellItem.identifier)
collectionView.backgroundColor = .clear
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
///Container view to hold collectionview to render pagination indexes and to handler accessibility.
private let collectionContainerView = PaginationContainer()
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
///Previous button to select previous page
public let previousButton: PaginationButton = .init(type: .previous)
///Next button to select next page
public let nextButton: PaginationButton = .init(type: .next)
/// A callback when the page changes. Passes parameters (selectedPage).
public var onPageDidSelect: ((Int) -> Void)?
/// Total number of pages, allows limit ranging from 0 to 9999.
@Clamping(range: 0...9999)
public var total: Int {
didSet {
previousButton.isHidden = true
nextButton.isHidden = total <= 1
_selectedPageIndex = 0
setNeedsUpdate()
updateSelection()
}
}
///Selected active page number and clips to total pages if selected index is greater than the total pages.
public var selectedPage: Int {
set {
if newValue >= total {
_selectedPageIndex = total - 1
} else if newValue < 0 {
_selectedPageIndex = 0
} else {
_selectedPageIndex = max(newValue - 1, 0)
}
setNeedsUpdate()
updateSelection()
}
get {
_selectedPageIndex + 1 //Returns selected page value not index
}
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
collectionContainerView.addSubview(collectionView)
containerView.addSubview(previousButton)
containerView.addSubview(collectionContainerView)
containerView.addSubview(nextButton)
addSubview(containerView)
containerView
.pinTop()
.pinBottom()
.pinLeadingGreaterThanOrEqualTo()
.pinTrailingLessThanOrEqualTo()
.pinCenterX()
.width(maxWidth)
.height(44)
previousButton
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailingGreaterThanOrEqualTo(collectionContainerView.leadingAnchor)
collectionContainerView
.pinTrailingGreaterThanOrEqualTo(nextButton.leadingAnchor)
.pinTop()
.pinBottom()
collectionView
.height(VDSLayout.space4X)
.pinCenterY()
.pinCenterX()
collectionViewWidthAnchor = collectionView.width(constant: 92)
nextButton
.pinTop()
.pinBottom()
.pinTrailing()
nextButton.onClick = onbuttonTapped
previousButton.onClick = onbuttonTapped
previousButton.isHidden = true
flowLayout.$collectionViewWidth
.receive(on: RunLoop.main)
.sink { [weak self] value in
self?.collectionViewWidthAnchor?.constant = value //As cell width is dynamic i.e cell may contain 2 or 3 or 4 charcters. Make sure that all the visible cells are displayed.
}.store(in: &subscribers)
collectionContainerView.onAccessibilityIncrement = { [weak self] in
guard let self else { return }
self.selectedPage = max(0, self.selectedPage + 1)
}
collectionContainerView.onAccessibilityDecrement = { [weak self] in
guard let self else { return }
self.selectedPage = max(0, self.selectedPage - 1)
}
}
///Updating the accessiblity values i.e elements, label, value other items for the component.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityElements = [previousButton, collectionContainerView, nextButton]
collectionContainerView.accessibilityLabel = "Pagination containing \(total) pages"
collectionContainerView.accessibilityValue = "Page \(selectedPage) of \(total) selected"
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
nextButton.surface = surface
previousButton.surface = surface
collectionView.reloadData()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
///When previous/next button is tapped
private func onbuttonTapped(_ sender: UIButton) {
let isNextAction = sender == nextButton
_selectedPageIndex = if isNextAction { _selectedPageIndex + 1 } else { _selectedPageIndex - 1 }
updateSelection()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
guard let self else { return }
UIAccessibility.post(notification: .announcement, argument: "Page \(self.selectedPage) of \(self.total) selected")
}
}
///Refreshing the UI based on the selected page
private func updateSelection() {
guard _selectedPageIndex < total else { return }
//Need to make selected page as second element so scrolling previous index of the selected page to left
collectionView.scrollToItem(at: IndexPath(row: max(_selectedPageIndex - 1, 0), section: 0), at: .left, animated: false)
previousButton.isHidden = _selectedPageIndex == 0
nextButton.isHidden = _selectedPageIndex == total - 1
collectionView.reloadData()
verifyIfMaxDigitChanged()
}
///Identifying if there is any change in the digits of upcoming page
private func verifyIfMaxDigitChanged() {
let upperLimitPage = _selectedPageIndex + flowLayout.maxNumberOfColumns
let upperLimitDigits = upperLimitPage.digitCount //future value digits
switch (flowLayout.numberOfColumns, upperLimitDigits) {
case (_, 1), (_, 2):
flowLayout.numberOfColumns = 4
default:
flowLayout.numberOfColumns = 3
}
if upperLimitDigits != flowLayout.upperLimitDigits {
flowLayout.upperLimitDigits = upperLimitDigits
flowLayout.invalidateLayout()
collectionView.reloadData()
//Need to make selected page as second element so scrolling previous index of the selected page to left
collectionView.scrollToItem(at: IndexPath(row: max(_selectedPageIndex - 1, 0), section: 0), at: .left, animated: false)
}
}
}
extension Pagination: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
//--------------------------------------------------
// MARK: - UICollectionView Delegate & Datasource
//--------------------------------------------------
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { total }
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PaginationCellItem.identifier, for: indexPath) as? PaginationCellItem else { return UICollectionViewCell() }
cell.update(_selectedPageIndex, currentIndex: indexPath.row, surface: surface)
return cell
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard _selectedPageIndex != indexPath.row else { return }
_selectedPageIndex = indexPath.row
updateSelection()
onPageDidSelect?(selectedPage)
}
}
fileprivate extension Int {
//--------------------------------------------------
// MARK: - Extension on Int to identify number of digits in given number.
//--------------------------------------------------
var digitCount: Int {
numberOfDigits(in: self)
}
private func numberOfDigits(in number: Int) -> Int {
number < 10 && number >= 0 ? 1 : 1 + numberOfDigits(in: number/10)
}
}

View File

@ -0,0 +1,111 @@
//
// PaginationButton.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 05/03/24.
//
import UIKit
import VDSTokens
///This is customised button for Pagination view
@objc(PaginationButton)
open class PaginationButton: ButtonBase {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
/// Type of the PaginationButton
private var type: Type = .next
/// Button tint color configuration
private let buttonTintColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
/// Button title color configuration
private let buttonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
/// Button configuration for iOS 15+
@available(iOS 15.0, *)
private var buttonConfiguration: Button.Configuration {
var configuration = ButtonBase.Configuration.plain()
configuration.imagePadding = VDSLayout.space2X
configuration.imagePlacement = type == .next ? .trailing : .leading
configuration.titleAlignment = type == .next ? .trailing : .leading
configuration.contentInsets = .zero
return configuration
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// TextStyle used on the titleLabel.
open override var textStyle: TextStyle { TextStyle.boldBodySmall }
/// UIColor used on the titleLabel text.
open override var textColor: UIColor { buttonTextColorConfiguration.getColor(surface) }
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
init(type: Type) {
self.type = type
super.init()
}
required public init() {
super.init()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
if #available(iOS 15.0, *) {
configuration = buttonConfiguration
} else {
semanticContentAttribute = type == .next ? .forceRightToLeft : .forceLeftToRight
imageEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: VDSLayout.space2X)
}
contentHorizontalAlignment = type == .next ? .trailing : .leading
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
text = type.title
let color = buttonTintColorConfiguration.getColor(surface)
setImage(type.image(color), for: .normal)
tintColor = color
super.updateView()
}
}
extension PaginationButton {
//--------------------------------------------------
// MARK: - Enum to configure PaginationButton
//--------------------------------------------------
enum `Type` {
case previous, next
var title: String {
switch self {
case .next:
"Next"
case .previous:
"Previous"
}
}
private var imageSize: CGSize { Icon.Size.xsmall.dimensions }
///Image for the configuration type
func image(_ color: UIColor) -> UIImage? {
switch self {
case .previous:
UIImage.image(for: .paginationLeftArrow, color: color, renderingMode: .alwaysTemplate)?.resized(to: imageSize)
case .next:
UIImage.image(for: .paginationRightArrow, color: color, renderingMode: .alwaysTemplate)?.resized(to: imageSize)
}
}
}
}

View File

@ -0,0 +1,63 @@
//
// PaginationCellItem.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 05/03/24.
//
import UIKit
import VDSTokens
///This is customised view for Pagination cell item
final class PaginationCellItem: UICollectionViewCell {
///Identifier for the PaginationCellItem
static let identifier: String = String(describing: PaginationCellItem.self)
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
///Text color configuration for the element
private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
///Pagination index label
private var indexLabel: Label = Label().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.textAlignment = .center
$0.isAccessibilityElement = false
$0.numberOfLines = 1
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUp()
}
///Configuring the cell with default setup
private func setUp() {
let containerView = View()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(indexLabel)
contentView.addSubview(containerView)
containerView.pinToSuperView()
indexLabel.pinToSuperView()
indexLabel.widthGreaterThanEqualTo(VDSLayout.space5X)
contentView.backgroundColor = .clear
containerView.backgroundColor = .clear
indexLabel.backgroundColor = .clear
}
///Updating UI based on selected index, current index along with surface
func update(_ selectedIndex: Int, currentIndex: Int, surface: Surface) {
indexLabel.textStyle = selectedIndex == currentIndex ? .boldBodySmall : .bodySmall
indexLabel.text = "\(currentIndex + 1)"
indexLabel.textColor = textColorConfiguration.getColor(surface)
}
}

View File

@ -0,0 +1,34 @@
MM/DD/YYYY
----------------
Initial Brand 3.0 handoff
12/17/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
02/28/2022
----------------
- Change Page Item Active to Page Item Selected. All Active references changed to Selected.
03/01/2022
----------------
- Replaced Left and Right Arrow Non-Scaling icons with VDS Icon.
- Removed “weight” and “vector effect” from Anatomy frame.
08/10/2022
----------------
- Updated default and inverted prop to light and dark surface.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced focus border pixel and style & spacing values with tokens.
01/12/2023
----------------
- Removed “Page Item Selected” from Anatomy.

View File

@ -0,0 +1,45 @@
//
// PaginationContainerView.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 12/03/24.
//
import UIKit
///PaginationCollectionView is a container view that holds collectionview for displaying page indexes
final class PaginationContainer: View {
//--------------------------------------------------
// MARK: - Internal Properties
//--------------------------------------------------
///Notifies when accessibility increment is happend when user swipes up
var onAccessibilityIncrement: (() -> Void)?
///Notifies when accessibility decrement is happend when user swipes down
var onAccessibilityDecrement: (() -> Void)?
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
///Accessibilty traits for the Pagination view
override var accessibilityTraits: UIAccessibilityTraits {
get { [.adjustable] }
set { }
}
///Accessibilty increment
override func accessibilityIncrement() {
onAccessibilityIncrement?()
}
///Accessibilty decrement
override func accessibilityDecrement() {
onAccessibilityDecrement?()
}
/// Executed on initialization for this View.
override func setup() {
super.setup()
isAccessibilityElement = true
}
}

View File

@ -0,0 +1,92 @@
//
// PaginationFlowLayout.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 06/03/24.
//
import Foundation
import VDSTokens
import UIKit
///Customised flow layout for Pagination view
final class PaginationFlowLayout : UICollectionViewLayout {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
///Spacing between the pagination cells
private let spacingBetweenCell: CGFloat = VDSLayout.space1X
///Pre-defined sizes of the pagination cell based on number of digits.
private var upperLimitSize: CGSize {
switch upperLimitDigits {
case 1, 2: .init(width: 20, height: 16)
case 3: .init(width: 28, height: 16)
default: .init(width: 34, height: 16)
}
}
///Property to store the defined layout attributes.
private var itemCache : [UICollectionViewLayoutAttributes] = []
//--------------------------------------------------
// MARK: - Internal Properties
//--------------------------------------------------
///Maximum number of page indexes shown on UI
let maxNumberOfColumns: Int = 4
///Number of digits of the maximum page index.
var upperLimitDigits: Int = 0
///Number of page indexes shown on UI.
var numberOfColumns: Int = 4
///A property that publishes when there is change in collection view width.
@Published var collectionViewWidth: CGFloat = 0
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
///Preparing the layout collection attributes for pagination and updating the collectionview width.
override func prepare() {
guard let collectionView else { return }
itemCache.removeAll()
var xPos : CGFloat = 0
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = .init(origin: .init(x: xPos, y: 0), size: upperLimitSize)
xPos += (spacingBetweenCell + upperLimitSize.width)
itemCache.append(attributes)
}
guard !itemCache.isEmpty else { return }
var rightMostVisibleItem: UICollectionViewLayoutAttributes?
if numberOfColumns < itemCache.count {
rightMostVisibleItem = itemCache[max(numberOfColumns - 1, 0)]
} else {
rightMostVisibleItem = itemCache.last
}
if let rightMostVisibleItem {
collectionViewWidth = rightMostVisibleItem.frame.minX + rightMostVisibleItem.frame.width
}
}
///This will return the layout attributes for the elements in the defined rect
override func layoutAttributesForElements(in rect: CGRect)-> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
for attributes in itemCache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
///This will return the layout attributes at particular indexPath
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return itemCache[indexPath.row]
}
///Returns the collectionview content size
override var collectionViewContentSize: CGSize {
guard let lastAttribute = itemCache.last else { return super.collectionViewContentSize }
return .init(width: lastAttribute.frame.width + lastAttribute.frame.origin.x, height: 16)
}
}

View File

@ -99,7 +99,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
/// If provided, the RadioBox textAttributedText will be rendered.
open var textAttributedText: NSAttributedString? {
didSet {
textLabel.useAttributedText = !(textAttributedText?.string.isEmpty ?? true)
textLabel.attributedText = textAttributedText
setNeedsUpdate()
}
@ -114,7 +113,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
/// If provided, the RadioBox subTextAttributedText will be rendered.
open var subTextAttributedText: NSAttributedString? {
didSet {
subTextLabel.useAttributedText = !(subTextAttributedText?.string.isEmpty ?? true)
subTextLabel.attributedText = subTextAttributedText
setNeedsUpdate()
}
@ -129,7 +127,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
/// If provided, the RadioBox subTextRightAttributedText will be rendered.
open var subTextRightAttributedText: NSAttributedString? {
didSet {
subTextRightLabel.useAttributedText = !(subTextRightAttributedText?.string.isEmpty ?? true)
subTextRightLabel.attributedText = subTextRightAttributedText
setNeedsUpdate()
}

View File

@ -264,7 +264,6 @@ open class Tabs: View {
model.onClick?(tab.index)
self.selectedIndex = tab.index
self.onTabDidSelect?(tab.index)
let t = tabViews[tab.index]
}
}
}

View File

@ -12,7 +12,7 @@ import Combine
/// Base Class used to build out a Input controls.
@objc(VDSEntryField)
open class EntryFieldBase: Control, Changeable {
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
// MARK: - Initializers
@ -58,7 +58,7 @@ open class EntryFieldBase: Control, Changeable {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fillProportionally
$0.distribution = .fill
$0.alignment = .top
}
}()
@ -69,6 +69,20 @@ open class EntryFieldBase: Control, Changeable {
}
}()
internal var bottomContainerView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
internal var bottomContainerStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
}
}()
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
@ -92,11 +106,15 @@ open class EntryFieldBase: Control, Changeable {
}
internal var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOnlight, forState: .normal)
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
}
internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
@ -134,19 +152,45 @@ open class EntryFieldBase: Control, Changeable {
/// Whether not to show the error.
open var showError: Bool = false { didSet { setNeedsUpdate() } }
/// FormFieldValidator
internal var validator: (any FormFieldValidatorable)?
/// Whether or not to show the internal error
open var hasInternalError: Bool { !(validator?.isValid ?? true) }
/// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if showError {
if showError || hasInternalError {
state.insert(.error)
}
return state
}
}
open var errorText: String? {
didSet {
updateContainerView()
updateErrorLabel()
setNeedsUpdate()
}
}
open var internalErrorText: String? {
didSet {
updateContainerView()
updateErrorLabel()
setNeedsUpdate()
}
}
/// Override this to conveniently get/set the textfield(s).
open var text: String? {
get { nil }
set { fatalError("You MUST override EntryField's 'text' variable in your subclass.") }
}
open var errorText: String? { didSet { setNeedsUpdate() } }
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } }
@ -156,8 +200,19 @@ open class EntryFieldBase: Control, Changeable {
open var maxLength: Int? { didSet { setNeedsUpdate() } }
open var inputId: String? { didSet { setNeedsUpdate() } }
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
/// The text of this textField.
private var _value: String?
open var value: String? {
get { _value }
set {
if let newValue, newValue != _value {
_value = newValue
text = newValue
}
setNeedsUpdate()
}
}
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
@ -183,7 +238,7 @@ open class EntryFieldBase: Control, Changeable {
//create the wrapping view
heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
widthConstraint?.priority = .defaultHigh
heightConstraint?.priority = .defaultHigh
heightConstraint?.isActive = true
widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0)
@ -202,15 +257,26 @@ open class EntryFieldBase: Control, Changeable {
//add the view to add input fields
containerStackView.addArrangedSubview(controlContainerView)
containerStackView.addArrangedSubview(icon)
containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView)
//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
bottomContainer.addSubview(bottomContainerStackView)
bottomContainerStackView.pinToSuperView()
bottomContainerStackView.addArrangedSubview(errorLabel)
bottomContainerStackView.addArrangedSubview(helperLabel)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(container)
stackView.addArrangedSubview(errorLabel)
stackView.addArrangedSubview(helperLabel)
stackView.addArrangedSubview(bottomContainer)
stackView.setCustomSpacing(4, after: titleLabel)
stackView.setCustomSpacing(8, after: container)
stackView.setCustomSpacing(8, after: errorLabel)
stackView.setCustomSpacing(8, after: bottomContainer)
stackView
.pinTop()
@ -253,18 +319,26 @@ open class EntryFieldBase: Control, Changeable {
open override func updateView() {
super.updateView()
containerView.backgroundColor = backgroundColorConfiguration.getColor(self)
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderWidth = VDSFormControls.borderWidth
containerView.layer.cornerRadius = VDSFormControls.borderRadius
updateContainerView()
updateTitleLabel()
updateErrorLabel()
updateHelperLabel()
backgroundColor = surface.color
validator?.validate()
internalErrorText = validator?.errorMessage
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateContainerView() {
containerView.backgroundColor = backgroundColorConfiguration.getColor(self)
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderWidth = VDSFormControls.borderWidth
containerView.layer.cornerRadius = VDSFormControls.borderRadius
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
@ -272,6 +346,11 @@ open class EntryFieldBase: Control, Changeable {
open func getContainer() -> UIView {
return containerView
}
/// Container for the area in which helper or error text presents.
open func getBottomContainer() -> UIView {
return bottomContainerView
}
open func updateTitleLabel() {
@ -304,7 +383,16 @@ open class EntryFieldBase: Control, Changeable {
}
open func updateErrorLabel(){
if showError, let errorText {
if showError, hasInternalError, let errorText, let internalErrorText {
errorLabel.text = [internalErrorText, errorText].joined(separator: "\n")
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
icon.name = .error
icon.color = VDSColor.paletteBlack
icon.surface = surface
icon.isHidden = !isEnabled
} else if showError, let errorText {
errorLabel.text = errorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
@ -313,6 +401,15 @@ open class EntryFieldBase: Control, Changeable {
icon.color = VDSColor.paletteBlack
icon.surface = surface
icon.isHidden = !isEnabled
} else if hasInternalError, let internalErrorText {
errorLabel.text = internalErrorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
icon.name = .error
icon.color = VDSColor.paletteBlack
icon.surface = surface
icon.isHidden = !isEnabled
} else {
icon.isHidden = true
errorLabel.isHidden = true

View File

@ -77,6 +77,18 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
/// Representing the type of input.
open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } }
/// The text of this textField.
open override var text: String? {
get { textField.text }
set {
if let newValue, newValue != text {
textField.text = newValue
value = newValue
}
setNeedsUpdate()
}
}
var _showError: Bool = false
/// Whether not to show the error.
open override var showError: Bool {

View File

@ -5,7 +5,6 @@
// Created by Matt Bruce on 1/10/23.
//
import Foundation
import Foundation
import UIKit
import VDSTokens
@ -35,64 +34,152 @@ open class TextArea: EntryFieldBase {
//--------------------------------------------------
internal var minWidthConstraint: NSLayoutConstraint?
internal var textViewHeightConstraint: NSLayoutConstraint?
internal var inputFieldStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.spacing = 12
$0.spacing = VDSLayout.space3X
}
}()
internal var bottomView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
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
$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()
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
override var containerSize: CGSize { CGSize(width: 45, height: 88) }
override var containerSize: CGSize { CGSize(width: 182, height: 88) }
/// Enum used to describe the the height of TextArea.
public enum Height: String, CaseIterable {
case twoX = "2X"
case fourX = "4X"
case eightX = "8X"
var value: CGFloat {
switch self {
case .twoX:
88
case .fourX:
176
case .eightX:
352
}
}
}
/// The text of this textView
private var _text: String?
open override var text: String? {
get { textView.text }
set {
if let newValue, newValue != _text {
_text = newValue
textView.text = newValue
value = newValue
}
setNeedsUpdate()
}
}
/// UITextView shown in the TextArea.
open var textView = UITextView().with {
open var textView = TextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = TextStyle.bodyLarge.font
$0.sizeToFit()
$0.isScrollEnabled = false
}
/// Color configuration for the textView.
open var textViewTextColorConfiguration: AnyColorable = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false)
}.eraseToAnyColorable() { didSet { setNeedsUpdate() } }
open override var maxLength: Int? { willSet { countRule.maxLength = newValue }}
/// Color configuration for error icon.
internal var iconColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
}
/// Color configuration for character counter's highlight background color
internal var highlightBackgroundColor = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal)
}
/// Color configuration for character counter's highlight text color
internal var highlightTextColor = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal)
}
//--------------------------------------------------
// 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 = "TextArea"
validator = FormFieldValidator<TextArea>(field: self, rules: [.init(countRule)])
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width)
minWidthConstraint?.isActive = true
controlContainerView.addSubview(textView)
textView
.pinTop()
.pinLeading()
.pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh)
.pinBottom(0, .defaultHigh)
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64)
textView.isScrollEnabled = true
textView.autocorrectionType = .no
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
textViewHeightConstraint?.isActive = true
backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success)
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
textView.delegate = self
characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
bottomContainerStackView.spacing = VDSLayout.space2X
}
/// Resets to default settings.
open override func reset() {
super.reset()
textView.text = ""
characterCounterLabel.reset()
characterCounterLabel.textStyle = .bodySmall
setNeedsUpdate()
}
/// Container for the area in which the user interacts.
@ -104,9 +191,9 @@ open class TextArea: EntryFieldBase {
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
textView.isEditable = isEnabled
textView.textColor = textViewTextColorConfiguration.getColor(self)
textView.isEnabled = isEnabled
textView.surface = surface
//set the width constraints
if let width {
@ -118,6 +205,84 @@ open class TextArea: EntryFieldBase {
widthConstraint?.isActive = false
minWidthConstraint?.isActive = true
}
characterCounterLabel.text = getCharacterCounterText()
icon.size = .medium
icon.color = iconColorConfiguration.getColor(self)
containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
textView.isEditable = readOnly ? false : true
textView.backgroundColor = backgroundColorConfiguration.getColor(self)
textView.tintColor = iconColorConfiguration.getColor(self)
characterCounterLabel.surface = surface
highlightCharacterOverflow()
}
/// Container for the area showing helper text, error text, character count, maximum length value.
open override func getBottomContainer() -> UIView {
bottomView.addSubview(bottomStackView)
bottomStackView.pinToSuperView()
bottomStackView.addArrangedSubview(bottomContainerView)
bottomStackView.addArrangedSubview(characterCounterLabel)
return bottomView
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
if showError {
setAccessibilityLabel(for: [titleLabel, textView, errorLabel, helperLabel])
} else {
setAccessibilityLabel(for: [titleLabel, textView, helperLabel])
}
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func getCharacterCounterText() -> String? {
let count = textView.text.count
let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)"
if let maxLength, maxLength > 0 {
if count > maxLength {
return countStr
} else {
return ("\(countStr)" + "/" + "\(maxLength)")
}
} else {
return nil
}
}
open func highlightCharacterOverflow() {
let count = textView.text.count
guard let maxLength, maxLength > 0, count > maxLength else {
textView.textAttributes = nil
return
}
var textAttributes = [any LabelAttributeModel]()
let location = maxLength
let length = count - maxLength
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightBackgroundColor.getColor(self), isForegroundColor: false))
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightTextColor.getColor(self), isForegroundColor: true))
textView.textAttributes = textAttributes
}
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
var countRule = CharacterCountRule()
class CharacterCountRule: Rule {
var maxLength: Int?
var errorMessage: String = "You have exceeded the character limit."
func isValid(value: String?) -> Bool {
guard let text = value, let maxLength, maxLength > 0 else { return true }
return text.count <= maxLength
}
}
}
@ -126,27 +291,42 @@ extension TextArea: UITextViewDelegate {
// MARK: - UITextViewDelegate
//--------------------------------------------------
public func textViewDidChange(_ textView: UITextView) {
//dynamic textView Height sizing based on Figma
//if you want it to work "as-is" delete this code
//since it will autogrow with the current settings
if let textViewHeightConstraint, textView.isEditable {
let height = textView.frame.size.height
let constraintHeight = textViewHeightConstraint.constant
if height > constraintHeight {
if height > 64 && height < 152 {
textViewHeightConstraint.constant = 152
} else if height > 152 {
textViewHeightConstraint.constant = 328
} else {
textViewHeightConstraint.constant = 64
}
var height = textView.contentSize.height
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 {
textViewHeightConstraint.constant = Height.eightX.value
} else {
textViewHeightConstraint.constant = Height.twoX.value
}
}
//setting the value and firing control event
value = textView.text
sendActions(for: .valueChanged)
//The exceeding characters will be highlighted to help users correct their entry.
if let maxLength, maxLength > 0 {
// allow - 20% of character limit
let overflowLimit = Double(maxLength) * 0.20
let allowCharCount = Int(overflowLimit) + maxLength
}
if textView.text.count <= allowCharCount {
highlightCharacterOverflow()
//setting the value and firing control event
text = textView.text
sendActions(for: .valueChanged)
} else {
textView.text.removeLast()
highlightCharacterOverflow()
}
} else {
//setting the value and firing control event
text = textView.text
sendActions(for: .valueChanged)
}
}
}

View File

@ -0,0 +1,38 @@
MM/DD/YYYY
----------------
- Initial Brand 3.0 handoff
12/27/2021
----------------
- Removed Max idth Updated the SPECS with FormControl tokens
02/25/2022
----------------
- Replaced Info and Error Non-Scaling icons with VDS Icon.
- Removed “weight” and “vector effect” from Anatomy and States.
07/27/2022
----------------
- Added Configurations section with transparentBackground principles.
08/10/2022
----------------
- Updated default and inverted prop to light and dark surface.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced form border and focus border pixel values and style & spacing with tokens.
01/18/2023
----------------
- Updated Anatomy items:
- Added “Highlight” to item #10
- Changed item #7 to “Tooltip” from “Tooltip Component”
04/12/2023
----------------
- Updated hex colors for updated feedback tokens in error states.

View File

@ -0,0 +1,151 @@
//
// TextView.swift
// VDS
//
// Created by Matt Bruce on 2/29/24.
//
import Foundation
import UIKit
import Combine
import VDSTokens
@objc(VDSTextView)
open class TextView: UITextView, ViewProtocol {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero, textContainer: nil)
initialSetup()
}
public override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
initialSetup()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
initialSetup()
}
//--------------------------------------------------
// MARK: - Combine Properties
//--------------------------------------------------
/// Set of Subscribers for any Publishers for this Control.
open var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
open var surface: Surface = .light { didSet { setNeedsUpdate() } }
/// Array of LabelAttributeModel objects used in rendering the text.
open var textAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } }
/// TextStyle used on the titleLabel.
open var textStyle: TextStyle { .defaultStyle }
/// Will determine if a scaled font should be used for the titleLabel font.
open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } }
open var isEnabled: Bool = true { didSet { setNeedsUpdate() } }
open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false)
}.eraseToAnyColorable(){ didSet { setNeedsUpdate() }}
open override var textColor: UIColor? {
get { textColorConfiguration.getColor(self) }
set { }
}
override public var text: String! {
get { super.text }
set {
super.text = newValue
updateLabel()
}
}
override public var textAlignment: NSTextAlignment {
didSet {
if textAlignment != oldValue {
// Text alignment can be part of our paragraph style, so we may need to
// re-style when changed
updateLabel()
}
}
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open func initialSetup() {
if !initialSetupPerformed {
initialSetupPerformed = true
backgroundColor = .clear
translatesAutoresizingMaskIntoConstraints = false
accessibilityCustomActions = []
setup()
setNeedsUpdate()
}
}
open func setup() {
translatesAutoresizingMaskIntoConstraints = false
}
open func updateView() {
updateLabel()
}
open func updateAccessibility() {}
open func reset() {
shouldUpdateView = false
surface = .light
text = nil
accessibilityCustomActions = []
shouldUpdateView = true
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateLabel() {
//clear the arrays holding actions
accessibilityCustomActions = []
if let text, !text.isEmpty {
//create the primary string
let mutableText = NSMutableAttributedString.mutableText(for: text,
textStyle: textStyle,
useScaledFont: useScaledFont,
textColor: textColor!,
alignment: textAlignment,
lineBreakMode: .byWordWrapping)
//apply any attributes
if let attributes = textAttributes {
mutableText.apply(attributes: attributes)
}
attributedText = mutableText
} else {
attributedText = nil
}
}
}

View File

@ -10,7 +10,39 @@ import VDSTokens
import UIKit
@objc(VDSTileContainer)
open class TileContainer: Control {
open class TileContainer: TileContainerBase<TileContainer.Padding> {
/// Enum used to describe the padding choices used for this component.
public enum Padding: DefaultValuing {
case padding2X
case padding4X
case padding6X
case padding8X
case padding12X
case custom(CGFloat)
public static var defaultValue: Self { .padding4X }
public var value: CGFloat {
switch self {
case .padding2X:
return VDSLayout.space2X
case .padding4X:
return VDSLayout.space4X
case .padding6X:
return VDSLayout.space6X
case .padding8X:
return VDSLayout.space8X
case .padding12X:
return VDSLayout.space12X
case .custom(let padding):
return padding
}
}
}
}
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
//--------------------------------------------------
// MARK: - Initializers
@ -52,33 +84,6 @@ open class TileContainer: Control {
case gradient(String, String)
case none
}
/// Enum used to describe the padding choices used for this component.
public enum Padding {
case padding2X
case padding4X
case padding6X
case padding8X
case padding12X
case custom(CGFloat)
public var value: CGFloat {
switch self {
case .padding2X:
return VDSLayout.space2X
case .padding4X:
return VDSLayout.space4X
case .padding6X:
return VDSLayout.space6X
case .padding8X:
return VDSLayout.space8X
case .padding12X:
return VDSLayout.space12X
case .custom(let padding):
return padding
}
}
}
/// Enum used to describe the aspect ratios used for this component.
public enum AspectRatio: String, CaseIterable {
@ -129,7 +134,7 @@ open class TileContainer: Control {
open var backgroundEffect: BackgroundEffect = .none { didSet { setNeedsUpdate() } }
/// Sets the inside padding for the component
open var padding: Padding = .padding4X { didSet { setNeedsUpdate() } }
open var padding: PaddingType = PaddingType.defaultValue { didSet { setNeedsUpdate() } }
/// Applies a background color if backgroundImage prop fails or has trouble loading.
open var imageFallbackColor: Surface = .light { didSet { setNeedsUpdate() } }
@ -183,9 +188,15 @@ open class TileContainer: Control {
// MARK: - Configuration
//--------------------------------------------------
private let cornerRadius = VDSFormControls.borderRadius * 2
private var backgroundColorConfiguration = BackgroundColorConfiguration()
private var dropshadowConfiguration = DropshadowConfiguration()
private let dropShadowConfiguration = DropShadowConfiguration().with {
$0.shadowColorConfiguration = SurfaceColorConfiguration().with {
$0.lightColor = VDSColor.elementsPrimaryOnlight
}.eraseToAnyColorable()
$0.shadowOffsetConfiguration = .init(.init(width: 0, height: 6), .zero)
$0.shadowRadiusConfiguration = .init(3.0, 0.0)
$0.shadowOpacityConfiguration = .init(0.01, 0.0)
}
private var borderColorConfiguration = SurfaceColorConfiguration().with {
$0.lightColor = VDSColor.elementsLowcontrastOnlight
@ -228,7 +239,12 @@ open class TileContainer: Control {
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
backgroundImageView.pin(layoutGuide)
backgroundImageView
.pinTop(layoutGuide.topAnchor)
.pinLeading(layoutGuide.leadingAnchor)
.pinTrailing(layoutGuide.trailingAnchor)
.pinBottom(layoutGuide.bottomAnchor, 0, .defaultLow)
backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true
@ -245,6 +261,7 @@ open class TileContainer: Control {
layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius
clipsToBounds = true
}
/// Resets to default settings.
@ -252,7 +269,6 @@ open class TileContainer: Control {
super.reset()
shouldUpdateView = false
color = .white
padding = .padding4X
aspectRatio = .ratio1x1
imageFallbackColor = .light
width = nil
@ -310,13 +326,20 @@ open class TileContainer: Control {
heightConstraint?.isActive = false
}
if showDropShadows, surface == .light {
addDropShadow(dropshadowConfiguration)
addDropShadow(dropShadowConfiguration)
} else {
removeDropShadows()
}
applyBackgroundEffects()
}
/// Used to update frames for the added CAlayers to our view
open override func layoutSubviews() {
super.layoutSubviews()
dropShadowLayers?.forEach { $0.frame = bounds }
gradientLayers?.forEach { $0.frame = bounds }
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
@ -394,20 +417,11 @@ open class TileContainer: Control {
}
extension TileContainer {
struct DropshadowConfiguration: Dropshadowable {
var shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().with {
$0.lightColor = VDSColor.elementsPrimaryOnlight
}.eraseToAnyColorable()
var shadowOpacity: CGFloat = 0.01
var shadowOffset: CGSize = .init(width: 0, height: 6)
var shadowRadius: CGFloat = 3
}
extension TileContainerBase {
final class BackgroundColorConfiguration: ObjectColorable {
typealias ObjectType = TileContainer
typealias ObjectType = TileContainerBase
let primaryColorConfig = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark)
let secondaryColorConfig = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
@ -417,7 +431,7 @@ extension TileContainer {
required init() { }
func getColor(_ object: TileContainer) -> UIColor {
func getColor(_ object: ObjectType) -> UIColor {
switch object.color {
case .primary:
primaryColorConfig.getColor(object.surface)

View File

@ -16,7 +16,38 @@ import Combine
/// while it can include an arrow CTA, it does not require one in order to
/// function.
@objc(VDSTilelet)
open class Tilelet: TileContainer {
open class Tilelet: TileContainerBase<Tilelet.Padding> {
/// Enum used to describe the padding choices used for this component.
public enum Padding: String, DefaultValuing, CaseIterable {
case small
case large
public static var defaultValue: Self { .large }
public var value: CGFloat {
switch self {
case .small:
return UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space4X
case .large:
return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space6X
}
}
fileprivate var titleLockupBottomSpacing: CGFloat {
switch self.value {
case VDSLayout.space3X:
return VDSLayout.space4X
case VDSLayout.space4X:
return VDSLayout.space6X
case VDSLayout.space4X:
return VDSLayout.space8X
default:
return VDSLayout.space4X
}
}
}
//--------------------------------------------------
// MARK: - Initializers
@ -39,6 +70,7 @@ open class Tilelet: TileContainer {
/// Enum to represent the Vertical Layout of the Text.
public enum TextPosition: String, CaseIterable {
case top
case middle
case bottom
}
@ -72,7 +104,7 @@ open class Tilelet: TileContainer {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
//--------------------------------------------------
public override var onClickSubscriber: AnyCancellable? {
didSet {
isAccessibilityElement = onClickSubscriber != nil
@ -82,6 +114,27 @@ open class Tilelet: TileContainer {
/// Title lockup positioned in the contentView.
open var titleLockup = TitleLockup().with {
$0.standardStyleConfiguration = .init(styleConfigurations: [
.init(deviceType: .iPhone,
titleStandardStyles: [.bodySmall],
spacingConfigurations: [
.init(otherStandardStyles: [.bodySmall],
topSpacing: VDSLayout.space1X,
bottomSpacing: VDSLayout.space1X)
]),
.init(deviceType: .iPhone,
titleStandardStyles: [.bodyMedium],
spacingConfigurations: [
.init(otherStandardStyles: [.bodyMedium],
topSpacing: VDSLayout.space1X,
bottomSpacing: VDSLayout.space1X)
]),
.init(deviceType: .iPhone,
titleStandardStyles: [.bodyLarge],
spacingConfigurations: [
.init(otherStandardStyles: [.bodyLarge],
topSpacing: VDSLayout.space1X,
bottomSpacing: VDSLayout.space1X)
]),
.init(deviceType: .iPhone,
titleStandardStyles: [.titleSmall],
spacingConfigurations: [
@ -103,6 +156,27 @@ open class Tilelet: TileContainer {
topSpacing: VDSLayout.space3X,
bottomSpacing: VDSLayout.space3X)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.bodySmall],
spacingConfigurations: [
.init(otherStandardStyles: [.bodySmall],
topSpacing: VDSLayout.space2X,
bottomSpacing: VDSLayout.space2X)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.bodyMedium],
spacingConfigurations: [
.init(otherStandardStyles: [.bodyMedium],
topSpacing: VDSLayout.space1X,
bottomSpacing: VDSLayout.space1X)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.bodyLarge],
spacingConfigurations: [
.init(otherStandardStyles: [.bodyLarge],
topSpacing: VDSLayout.space1X,
bottomSpacing: VDSLayout.space1X)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.titleSmall, .titleMedium],
spacingConfigurations: [
@ -176,6 +250,9 @@ open class Tilelet: TileContainer {
/// If set, this is used to render the subTitleLabel of the TitleLockup.
open var subTitleModel: SubTitleModel? { didSet { setNeedsUpdate() } }
/// If set, this is used to render the eyebrowLabel of the TitleLockup.
open var eyebrowModel: EyebrowModel? { didSet { setNeedsUpdate() } }
//only 1 Icon can be active
private var _descriptiveIconModel: DescriptiveIcon?
@ -206,6 +283,17 @@ open class Tilelet: TileContainer {
//--------------------------------------------------
internal var titleLockupWidthConstraint: NSLayoutConstraint?
internal var titleLockupTrailingConstraint: NSLayoutConstraint?
internal var titleLockupTopConstraint: NSLayoutConstraint?
internal var titleLockupBottomConstraint: NSLayoutConstraint?
internal var titleLockupTopGreaterThanConstraint: NSLayoutConstraint?
internal var titleLockupBottomGreaterThanConstraint: NSLayoutConstraint?
internal var titleLockupCenterYConstraint: NSLayoutConstraint?
internal var titleLockupTitleLabelBottomConstraint: NSLayoutConstraint?
//Truncation constraints
internal var badgeLabelHeightGreaterThanConstraint: NSLayoutConstraint?
internal var titleLockupEyebrowLabelHeightGreaterThanConstraint: NSLayoutConstraint?
internal var titleLockupTitleLabelHeightGreaterThanConstraint: NSLayoutConstraint?
internal var titleLockupSubTitleLabelHeightGreaterThanConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Overrides
@ -230,12 +318,17 @@ open class Tilelet: TileContainer {
titleLockupContainerView.addSubview(titleLockup)
titleLockup
.pinTop()
.pinLeading()
.pinBottom()
titleLockupTopConstraint = titleLockup.topAnchor.constraint(equalTo: titleLockupContainerView.topAnchor)
titleLockupTopConstraint?.activate()
titleLockupBottomConstraint = titleLockupContainerView.bottomAnchor.constraint(equalTo: titleLockup.bottomAnchor)
titleLockupBottomConstraint?.activate()
titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: titleLockupContainerView.trailingAnchor)
titleLockupTrailingConstraint?.isActive = true
titleLockupTrailingConstraint?.activate()
titleLockupBottomGreaterThanConstraint = titleLockupContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: titleLockup.bottomAnchor)
titleLockupTopGreaterThanConstraint = titleLockup.topAnchor.constraint(greaterThanOrEqualTo: titleLockupContainerView.topAnchor)
titleLockupCenterYConstraint = titleLockup.centerYAnchor.constraint(equalTo: titleLockupContainerView.centerYAnchor)
iconContainerView.addSubview(descriptiveIcon)
iconContainerView.addSubview(directionalIcon)
@ -249,6 +342,47 @@ open class Tilelet: TileContainer {
.pinTop()
.pinBottom()
badge.bottomAnchor.constraint(equalTo: badge.label.bottomAnchor, constant: 2).activate()
/**
Truncation:
If a Tilelet has only a Title or a Subtitle, then the Title or Subtitle is truncated and appended with an ellipsis when there is not enough space to display the full text.
If a Tilelet has both Title and Subtitle, then only Subtitle will be truncated.
If a Tilelet has Badge, Title and Subtitle, then Subtitle will be truncated first and Badge will be truncated second. Title will be truncated last (lowest priority).
Atleast one line text based on priority
Minimum bottom space below Badge is 4px; less than 4px results in truncation.
*/
let labelPriority = UILayoutPriority.defaultHigh.rawValue
titleLockup.titleLabel.setContentCompressionResistancePriority(UILayoutPriority(labelPriority), for: .vertical)
badge.label.setContentCompressionResistancePriority(UILayoutPriority(labelPriority-1), for: .vertical)
titleLockup.subTitleLabel.setContentCompressionResistancePriority(UILayoutPriority(labelPriority-2), for: .vertical)
titleLockup.eyebrowLabel.setContentCompressionResistancePriority(UILayoutPriority(labelPriority-3), for: .vertical)
titleLockup.titleLabel.setContentHuggingPriority(UILayoutPriority(labelPriority), for: .vertical)
badge.label.setContentHuggingPriority(UILayoutPriority(labelPriority-1), for: .vertical)
titleLockup.subTitleLabel.setContentHuggingPriority(UILayoutPriority(labelPriority-2), for: .vertical)
titleLockup.eyebrowLabel.setContentHuggingPriority(UILayoutPriority(labelPriority-3), for: .vertical)
/**
Added these constraints for:
At fixed width & height if all the labels(Badge, Eyebrow, Title, Subtitle) are having more number of lines then we should display atleast one line of content per label instead of pushing labels out of bounds.
So adding minimum single line height constraint
*/
badgeLabelHeightGreaterThanConstraint = badge.label.heightGreaterThanEqualTo(constant: badge.label.minimumLineHeight)
badgeLabelHeightGreaterThanConstraint?.priority = .defaultHigh
badgeLabelHeightGreaterThanConstraint?.activate()
titleLockupEyebrowLabelHeightGreaterThanConstraint = titleLockup.eyebrowLabel.heightGreaterThanEqualTo(constant: titleLockup.eyebrowLabel.minimumLineHeight)
titleLockupEyebrowLabelHeightGreaterThanConstraint?.priority = .defaultHigh
titleLockupEyebrowLabelHeightGreaterThanConstraint?.activate()
titleLockupTitleLabelHeightGreaterThanConstraint = titleLockup.titleLabel.heightGreaterThanEqualTo(constant: titleLockup.titleLabel.minimumLineHeight)
titleLockupTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
titleLockupTitleLabelHeightGreaterThanConstraint?.activate()
titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight)
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
}
/// Resets to default settings.
@ -261,7 +395,7 @@ open class Tilelet: TileContainer {
titleModel = nil
subTitleModel = nil
descriptiveIconModel = nil
directionalIconModel = nil
directionalIconModel = nil
shouldUpdateView = true
setNeedsUpdate()
}
@ -273,7 +407,11 @@ open class Tilelet: TileContainer {
updateBadge()
updateTitleLockup()
updateIcons()
///Content-driven height Tilelets - Minimum height is configurable.
///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments.
if width != nil && (aspectRatio != .none || height != nil) {
updateTextPositionAlignment()
}
layoutIfNeeded()
}
@ -294,6 +432,7 @@ open class Tilelet: TileContainer {
badge.numberOfLines = badgeModel.numberOfLines
badge.surface = badgeModel.surface
badge.maxWidth = badgeModel.maxWidth
badgeLabelHeightGreaterThanConstraint?.constant = badge.label.minimumLineHeight
if badgeContainerView.superview == nil {
stackView.insertArrangedSubview(badgeContainerView, at: 0)
setNeedsLayout()
@ -306,7 +445,11 @@ open class Tilelet: TileContainer {
private func updateTitleLockup() {
var showTitleLockup = false
if let eyebrowModel, !eyebrowModel.text.isEmpty {
showTitleLockup = true
}
if let titleModel, !titleModel.text.isEmpty {
showTitleLockup = true
}
@ -350,6 +493,7 @@ open class Tilelet: TileContainer {
}
//set models
titleLockup.eyebrowModel = eyebrowModel?.toTitleLockupEyebrowModel()
titleLockup.titleModel = titleModel?.toTitleLockupTitleModel()
titleLockup.subTitleModel = subTitleModel?.toTitleLockupSubTitleModel()
@ -360,6 +504,9 @@ open class Tilelet: TileContainer {
} else {
removeFromSuperview(titleLockupContainerView)
}
titleLockupEyebrowLabelHeightGreaterThanConstraint?.constant = titleLockup.eyebrowLabel.minimumLineHeight
titleLockupTitleLabelHeightGreaterThanConstraint?.constant = titleLockup.titleLabel.minimumLineHeight
titleLockupSubTitleLabelHeightGreaterThanConstraint?.constant = titleLockup.subTitleLabel.minimumLineHeight
}
private func updateIcons() {
@ -392,7 +539,7 @@ open class Tilelet: TileContainer {
view = titleLockupContainerView
}
if let view {
stackView.setCustomSpacing(padding.tiletSpacing, after: view)
stackView.setCustomSpacing(padding.titleLockupBottomSpacing, after: view)
}
if iconContainerView.superview == nil {
stackView.addArrangedSubview(iconContainerView)
@ -402,21 +549,33 @@ open class Tilelet: TileContainer {
removeFromSuperview(iconContainerView)
}
}
}
extension TileContainer.Padding {
fileprivate var tiletSpacing: CGFloat {
switch self {
case .padding2X:
return 16
case .padding4X:
return 24
case .padding6X:
return 32
case .padding8X:
return 48
default:
return 16
private func updateTextPositionAlignment() {
switch textPostion {
case .top:
titleLockupTopConstraint?.activate()
titleLockupTopGreaterThanConstraint?.deactivate()
titleLockupBottomConstraint?.deactivate()
titleLockupBottomGreaterThanConstraint?.activate()
titleLockupCenterYConstraint?.deactivate()
case .middle:
titleLockupTopConstraint?.deactivate()
titleLockupTopGreaterThanConstraint?.activate()
titleLockupBottomConstraint?.deactivate()
titleLockupBottomGreaterThanConstraint?.activate()
titleLockupCenterYConstraint?.activate()
case .bottom:
titleLockupTopConstraint?.deactivate()
titleLockupTopGreaterThanConstraint?.activate()
titleLockupBottomConstraint?.activate()
titleLockupBottomGreaterThanConstraint?.deactivate()
titleLockupCenterYConstraint?.deactivate()
}
}
}
extension Label {
///To calculate label single line height
fileprivate var minimumLineHeight: CGFloat { textStyle.lineHeight }
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import UIKit
extension Tilelet {
@ -26,12 +27,16 @@ extension Tilelet {
/// Max width that will be used for the badge.
public var maxWidth: CGFloat?
public init(text: String, fillColor: Badge.FillColor = .red, surface: Surface = .light, numberOfLines: Int = 0, maxWidth: CGFloat? = nil) {
/// LineBreakMode used in Badge label.
public var lineBreakMode: NSLineBreakMode
public init(text: String, fillColor: Badge.FillColor = .red, surface: Surface = .light, numberOfLines: Int = 0, maxWidth: CGFloat? = nil, lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
self.text = text
self.fillColor = fillColor
self.surface = surface
self.numberOfLines = numberOfLines
self.maxWidth = maxWidth
self.lineBreakMode = lineBreakMode
}
}
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import UIKit
extension Tilelet {
/// Model that represents the options available for the sub title label.
@ -14,11 +15,12 @@ extension Tilelet {
// MARK: - Enums
//--------------------------------------------------
/// Enum used to describe the textStyle of the subTitle label.
public enum StandardStyle: String, EnumSubset {
public enum OtherStandardStyle: String, EnumSubset {
case bodyLarge
case bodyMedium
case bodySmall
case titleSmall
case titleMedium
public var defaultValue: TitleLockup.OtherStandardStyle { .bodySmall }
}
//--------------------------------------------------
@ -28,7 +30,7 @@ extension Tilelet {
public var text: String = ""
/// Text style that will be used for the subTitle label.
public var standardStyle: StandardStyle = .bodySmall
public var otherStandardStyle: OtherStandardStyle = .bodySmall
/// Text attributes that will be used for the subTitle label.
public var textAttributes: [any LabelAttributeModel]?
@ -36,17 +38,22 @@ extension Tilelet {
/// Text color that will be used for the subTitle label.
public var textColor: Use = .primary
/// LineBreakMode used in Badge label.
public var lineBreakMode: NSLineBreakMode
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public init(text: String,
otherStandardStyle: OtherStandardStyle = .bodySmall,
textColor: Use = .primary,
textAttributes: [any LabelAttributeModel]? = nil,
standardStyle: StandardStyle = .bodySmall) {
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
self.text = text
self.otherStandardStyle = otherStandardStyle
self.textAttributes = textAttributes
self.textColor = textColor
self.standardStyle = standardStyle
self.lineBreakMode = lineBreakMode
}
//--------------------------------------------------
@ -55,9 +62,9 @@ extension Tilelet {
/// Converts this type of model to a TitleLockup.SubTitleModel.
public func toTitleLockupSubTitleModel() -> TitleLockup.SubTitleModel {
TitleLockup.SubTitleModel(text: text,
standardStyle: standardStyle.value,
otherStandardStyle: otherStandardStyle.value,
textColor: textColor,
textAttributes: textAttributes)
textAttributes: textAttributes, lineBreakMode: lineBreakMode)
}
}
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import UIKit
extension Tilelet {
/// Model that represents the options available for the title label.
@ -19,7 +20,10 @@ extension Tilelet {
case titleLarge
case titleMedium
case titleSmall
case bodyLarge
case bodyMedium
case bodySmall
public var defaultValue: TitleLockup.TitleStandardStyle { .titleSmall }
}
//--------------------------------------------------
@ -28,21 +32,30 @@ extension Tilelet {
/// Text that will be used for the title label.
public var text: String = ""
/// Used in combination with standardStyle to set the textStyle that will be used for the title label.
public var isBold: Bool = false
/// Text attributes that will be used for the title label.
public var textAttributes: [any LabelAttributeModel]?
/// Text style that will be used for the title label.
public var standardStyle: StandardStyle = .titleSmall
/// LineBreakMode used in Badge label.
public var lineBreakMode: NSLineBreakMode
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public init(text: String,
textAttributes: [any LabelAttributeModel]? = nil,
standardStyle: StandardStyle = .titleSmall) {
isBold: Bool = true,
standardStyle: StandardStyle = .titleSmall,
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
self.text = text
self.textAttributes = textAttributes
self.standardStyle = standardStyle
self.isBold = isBold
self.lineBreakMode = lineBreakMode
}
//--------------------------------------------------
@ -52,7 +65,7 @@ extension Tilelet {
public func toTitleLockupTitleModel() -> TitleLockup.TitleModel {
TitleLockup.TitleModel(text: text,
textAttributes: textAttributes,
standardStyle: standardStyle.value)
isBold: isBold, standardStyle: standardStyle.value, lineBreakMode: lineBreakMode)
}
}
}

View File

@ -0,0 +1,55 @@
//
// TiletEyebrowModel.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 13/03/24.
//
import Foundation
import UIKit
extension Tilelet {
/// Model that represents the options available for the eyebrow label.
public struct EyebrowModel {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Text that will be used for the eyebrow label.
public var text: String = ""
/// Used in combination with standardStyle to set the textStyle that will be used for the eyebrow label.
public var isBold: Bool = false
/// Text attributes that will be used for the eyebrow label.
public var textAttributes: [any LabelAttributeModel]?
/// Text style that will be used for the eyebrow label. If subtitle standard style
public var standardStyle: Tilelet.SubTitleModel.OtherStandardStyle = .titleSmall
/// LineBreakMode used in Badge label.
public var lineBreakMode: NSLineBreakMode
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public init(text: String,
textAttributes: [any LabelAttributeModel]? = nil,
isBold: Bool = true,
standardStyle: Tilelet.SubTitleModel.OtherStandardStyle = .bodySmall,
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
self.text = text
self.textAttributes = textAttributes
self.standardStyle = standardStyle
self.isBold = isBold
self.lineBreakMode = lineBreakMode
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
/// Converts this type of model to a TitleLockup.TitleModel.
public func toTitleLockupEyebrowModel() -> TitleLockup.EyebrowModel {
TitleLockup.EyebrowModel(text: text, isBold: isBold, standardStyle: standardStyle.value, textAttributes: textAttributes)
}
}
}

View File

@ -0,0 +1,110 @@
MM/DD/YYYY
----------------
08/31/2022
----------------
- Updates to Typography and default Title Lockup configurations:
- Default
- Regular title updated to bold weight
- Subtitle updated to primary color
- Inverted
- Regular title updated to bold weight
- Subtitle updated to primary color
07/15/2022
----------------
- Updates to Typography and default Title Lockup configurations:
- Default
- Bold Title updated to Regular weight
- Subtitle updated to secondary color
- Inverted
- Bold Title updated to Regular weight
- Subtitle updated to secondary color
04/27/2022
----------------
- Updates to Title Lockup configurations.
04/14/2022
----------------
- Updates to Tilelet Group spec based on 4/14 handoff feedback.
04/04/2022
----------------
- Visuals added to Tilelet Group spec.
04/01/2022
----------------
- Updates based on 3/31 handoff feedback.
03/30/2022
----------------
- Tilelet added to Test App.
03/29/2022
----------------
- Interactive states updated.
03/22/2022
----------------
- Layout and spacing page added.
03/21/2022
----------------
- Updated anatomy, configuration, and element pages.
02/21/2022
----------------
- Title Lockups width properties added.
09/14/2022
----------------
- States section updated to reflect states inherited from Tile Container
- Dev note added to Layouts.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced focus border pixel and style & spacing values with tokens.
06/19/2023
----------------
- Updated anatomy element names to be local to Tilelet and indicated the core component that powers them under the hood.
09/21/2023
----------------
- Updated Anatomy to include Title Lockup with Eyebrow and Tooltip
- Updated Configurations to include Title Lockup section
- Updated Layout and spacing to include Alignment (Left, Center) and textPosition (Middle)
- Updated Elements to include Eyebrow and uniform 1:1 pairings
10/09/2023
----------------
- Updated Configurations VDS Title Lockup and VDS Tile Container sections.
10/13/2023
----------------
- SPEC format changes to Anatomy to include nested component configurations
- Removed Configurations page
11/20/2023
----------------
- Updated visuals to reflect new corner radius value - 12px
- Updated focus border corner radius to 14px
- View changes
12/14/2023
----------------
- Added secondary, primary backgroundColor options
- Relabeled configurations section headings to use property names
- Relabeled surface sections to use “surface:” labeling convention
- Deprecated gray backgroundColor
- View changes
02/08/2024
----------------
- Added external-icon option to Descriptive Icon in Configurations
- View changes

View File

@ -45,7 +45,7 @@ open class TitleLockup: View {
//--------------------------------------------------
private var otherStandardStyle: OtherStandardStyle {
if let subTitleModel, !subTitleModel.text.isEmpty {
return subTitleModel.standardStyle
return subTitleModel.otherStandardStyle
} else if let eyebrowModel, !eyebrowModel.text.isEmpty {
return eyebrowModel.standardStyle
} else {
@ -68,6 +68,7 @@ open class TitleLockup: View {
/// Label used to render the eyebrow model.
open var eyebrowLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentHuggingPriority(.required, for: .vertical)
}
/// Model used in rendering the eyebrow label.
@ -77,6 +78,7 @@ open class TitleLockup: View {
/// Label used to render the title model.
open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentHuggingPriority(.required, for: .vertical)
$0.accessibilityTraits.insert([.header])
}
@ -87,6 +89,7 @@ open class TitleLockup: View {
/// Label used to render the subtitle model.
open var subTitleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentHuggingPriority(.required, for: .vertical)
}
/// Model used in rendering the subtitle label.
@ -350,7 +353,7 @@ open class TitleLockup: View {
titleLabel.attributes = titleModel.textAttributes
titleLabel.numberOfLines = titleModel.numberOfLines
titleLabel.surface = surface
titleLabel.lineBreakMode = titleModel.lineBreakMode
addSubview(titleLabel)
titleLabel
.pinTop(previousView?.bottomAnchor ?? self.topAnchor, eyebrowLabel == previousView ? topSpacing : 0)
@ -369,7 +372,7 @@ open class TitleLockup: View {
subTitleLabel.attributes = subTitleModel.textAttributes
subTitleLabel.numberOfLines = subTitleModel.numberOfLines
subTitleLabel.surface = surface
subTitleLabel.lineBreakMode = subTitleModel.lineBreakMode
addSubview(subTitleLabel)
subTitleLabel
.pinTop(previousView?.bottomAnchor ?? self.topAnchor, (eyebrowLabel == previousView || titleLabel == previousView) ? bottomSpacing : 0)
@ -380,7 +383,7 @@ open class TitleLockup: View {
}
//pin the last view to the bottom of this view
previousView?.pinBottom(0, .defaultHigh)
previousView?.pinBottom(0)
//debugging for borders
eyebrowLabel.debugBorder(show: hasDebugBorder, color: .green)

View File

@ -6,6 +6,7 @@
//
import Foundation
import UIKit
extension TitleLockup {
/// Model that represents the options available for the sub title label.
@ -14,7 +15,7 @@ extension TitleLockup {
public var text: String
/// Standard style that will be used for the subTitle label.
public var standardStyle: OtherStandardStyle
public var otherStandardStyle: OtherStandardStyle
/// Text color used in the subtitle label.
public var textColor: Use
@ -25,20 +26,25 @@ extension TitleLockup {
/// Number of lines used in the subtitle label.
public var numberOfLines: Int
/// LineBreakMode used in subtitle label.
public var lineBreakMode: NSLineBreakMode
public init(text: String,
standardStyle: OtherStandardStyle = .bodyLarge,
otherStandardStyle: OtherStandardStyle = .bodyLarge,
textColor: Use = .primary,
textAttributes: [any LabelAttributeModel]? = nil,
numberOfLines: Int = 0) {
numberOfLines: Int = 0,
lineBreakMode: NSLineBreakMode = .byWordWrapping) {
self.text = text
self.standardStyle = standardStyle
self.otherStandardStyle = otherStandardStyle
self.textColor = textColor
self.textAttributes = textAttributes
self.numberOfLines = numberOfLines
self.lineBreakMode = lineBreakMode
}
/// TextStyle used to render the text.
public var textStyle: TextStyle { standardStyle.value.regular }
public var textStyle: TextStyle { otherStandardStyle.value.regular }
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import UIKit
extension TitleLockup {
/// Model that represents the options available for the sub title label.
@ -25,16 +26,21 @@ extension TitleLockup {
/// Number of lines that will be used for the title label.
public var numberOfLines: Int
/// LineBreakMode used in subtitle label.
public var lineBreakMode: NSLineBreakMode
public init(text: String,
textAttributes: [any LabelAttributeModel]? = nil,
isBold: Bool = true,
standardStyle: TitleStandardStyle = .featureXSmall,
numberOfLines: Int = 0) {
numberOfLines: Int = 0,
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
self.text = text
self.isBold = isBold
self.textAttributes = textAttributes
self.standardStyle = standardStyle
self.numberOfLines = numberOfLines
self.lineBreakMode = lineBreakMode
}
/// TextStyle used to render the text.

View File

@ -0,0 +1,39 @@
//
// UIImage+Icon.swift
// VDS
//
// Created by Matt Bruce on 3/11/24.
//
import Foundation
import UIKit
extension UIImage {
/// UIImage helper for finding images based on the Icon.Name which uses the internal BundleManager.
/// - Parameters:
/// - name: RawRepresentable.
/// - color: Color to Tint the image with
/// - renderingMode: UIImage Rendering mode.
/// - Returns: UIImage for this proecess
public static func image<T: RawRepresentable>(representing representable: T, color: UIColor? = nil, renderingMode: UIImage.RenderingMode = .alwaysOriginal) -> UIImage? where T.RawValue == String {
guard let image = BundleManager.shared.image(for: representable.rawValue) else { return nil }
guard let color else { return image }
return image.withTintColor(color, renderingMode: renderingMode)
}
/// Resizes image to a specific Size
/// - Parameter size: Size to resize
/// - Returns: Image that is resized
public func resized(to size: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
defer { UIGraphicsEndImageContext() }
draw(in: .init(origin: .zero, size: size))
return UIGraphicsGetImageFromCurrentImageContext()
}
}

View File

@ -0,0 +1,54 @@
//
// VDSLayout.swift
// VDS
//
// Created by Matt Bruce on 1/12/23.
//
import Foundation
/// Represents constants used that deal with layout.
public struct VDSLayout {
/// Enum used to describe the spacing constants.
public enum Spacing: String, CaseIterable, Valuing {
case space1X
case space2X
case space3X
case space4X
case space5X
case space6X
case space8X
case space12X
case space16X
case space24X
case space32X
public var value: CGFloat {
switch self {
case .space1X:
return 4
case .space2X:
return 8
case .space3X:
return 12
case .space4X:
return 16
case .space5X:
return 20
case .space6X:
return 24
case .space8X:
return 32
case .space12X:
return 48
case .space16X:
return 64
case .space24X:
return 96
case .space32X:
return 128
}
}
}
}

View File

@ -6,15 +6,17 @@
//
import Foundation
import UIKit
/// Enum that is matched up for the Verizon fonts.
public enum Font: String, FontProtocol {
public enum Font: FontProtocol {
case edsBold
case edsRegular
case dsLight
case etxBold
case etxRegular
case custom(UIFont)
public var fontName: String {
switch self {
case .edsBold:
@ -27,11 +29,32 @@ public enum Font: String, FontProtocol {
return "VerizonNHGeTX-Bold"
case .etxRegular:
return "VerizonNHGeTX-Regular"
case .custom(let font):
return font.fontName
}
}
public static var allCases: [Font] {
[.edsBold, .edsRegular, .dsLight, .etxBold, .etxRegular]
}
/// File Extension for each of the Font enums.
public var fontFileExtension: String {
return "otf"
}
/// Returns a UIFont for the fontName and size given.
/// - Parameters:
/// - size: Size of the font
/// - Returns: UIFont for the fontName and Size.
public func font(ofSize size: CGFloat) -> UIFont{
DispatchQueue.once(block: { self.register() })
switch self {
case .custom(let font):
return font
default:
guard let found = UIFont(name: self.fontName, size: size) else { return .systemFont(ofSize: size) }
return found
}
}
}

View File

@ -9,9 +9,10 @@ import Foundation
import UIKit
/// Used in Classes that require Fonts
public protocol FontProtocol: CaseIterable, RawRepresentable, Hashable {
public protocol FontProtocol {
var fontFileExtension: String { get }
var fontName: String { get }
static var allCases: [Self] { get }
}
extension FontProtocol {

View File

@ -0,0 +1,12 @@
//
// EnumValuing.swift
// VDS
//
// Created by Matt Bruce on 3/1/24.
//
import Foundation
public protocol DefaultValuing: Valuing {
static var defaultValue: Self { get }
}

View File

@ -0,0 +1,92 @@
//
// DropShadowable.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 16/02/24.
//
import Foundation
import UIKit
/**
DropShadowable protocol helps with the configuration values for adding drop shadows for light & dark surfaces.
*/
protocol DropShadowable {
///Shadow Color configuration for light and dark surfaces
var shadowColorConfiguration: AnyColorable { get set }
///Shadow Opacity configuration for light and dark surfaces
var shadowOpacityConfiguration: SurfaceConfigurationValue<CGFloat> { get set }
///Shadow Offset configuration for light and dark surfaces
var shadowOffsetConfiguration: SurfaceConfigurationValue<CGSize> { get set }
///Shadow Radius configuration for light and dark surfaces
var shadowRadiusConfiguration: SurfaceConfigurationValue<CGFloat> { get set }
}
/**
DropShadowableConfiguration protocol helps with multiple drop shadows configurations can be added to a view.
*/
protocol DropShadowableConfiguration {
///Configurations are the DropShadowable list, these are applied on the view
var configurations: [DropShadowable] { get }
}
/**
Extension on ViewProtocol for adding drop shadows & gradient layer on view.
*/
extension ViewProtocol where Self: UIView {
func addDropShadow(_ config: DropShadowable) {
addDropShadows([config])
}
func addDropShadows(_ configs: [DropShadowable]) {
removeDropShadows()
layer.backgroundColor = backgroundColor?.cgColor
layer.masksToBounds = false
for config in configs {
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
let shadowLayer = CALayer()
shadowLayer.shadowPath = shadowPath.cgPath
shadowLayer.frame = bounds
shadowLayer.position = .init(x: bounds.midX, y: bounds.midY)
shadowLayer.cornerRadius = layer.cornerRadius
shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor
shadowLayer.shadowOpacity = Float(config.shadowOpacityConfiguration.value(for: self))
shadowLayer.shadowOffset = config.shadowOffsetConfiguration.value(for: self)
shadowLayer.shadowRadius = config.shadowRadiusConfiguration.value(for: self)
shadowLayer.name = "dropShadowLayer"
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
layer.insertSublayer(shadowLayer, at: 0)
}
}
func removeDropShadows() {
layer.sublayers?.removeAll { $0.name == "dropShadowLayer" }
}
func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) {
removeGradientLayer()
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 1)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
gradientLayer.position = center
gradientLayer.shouldRasterize = true
gradientLayer.backgroundColor = UIColor.clear.cgColor
gradientLayer.rasterizationScale = UIScreen.main.scale
gradientLayer.cornerRadius = layer.cornerRadius
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.name = "gradientLayer"
layer.insertSublayer(gradientLayer, at: 0)
}
func removeGradientLayer() {
layer.sublayers?.removeAll { $0.name == "gradientLayer" }
}
//Helper variables to get gradient & shadow layers. These are exposed to update frame of layers when view's frame is updated.
var dropShadowLayers: [CALayer]? { layer.sublayers?.filter { $0.name == "dropShadowLayer" } }
var gradientLayers: [CAGradientLayer]? { layer.sublayers?.filter { $0.name == "gradientLayer" } as? [CAGradientLayer] }
}

View File

@ -1,64 +0,0 @@
//
// Dropshadowable.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 16/02/24.
//
import Foundation
import UIKit
protocol Dropshadowable {
var shadowColorConfiguration: AnyColorable { get set }
var shadowOpacity: CGFloat { get set }
var shadowOffset: CGSize { get set }
var shadowRadius: CGFloat { get set }
}
extension ViewProtocol where Self: UIView {
func addDropShadow(_ config: Dropshadowable) {
removeDropShadows()
layer.backgroundColor = backgroundColor?.cgColor
layer.masksToBounds = false
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
let shadowLayer = CALayer()
shadowLayer.shadowPath = shadowPath.cgPath
shadowLayer.frame = bounds
shadowLayer.position = center
shadowLayer.backgroundColor = UIColor.clear.cgColor
shadowLayer.cornerRadius = layer.cornerRadius
shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor
shadowLayer.shadowOpacity = Float(config.shadowOpacity)
shadowLayer.shadowOffset = .init(width: config.shadowOffset.width, height: config.shadowOffset.height)
shadowLayer.shadowRadius = config.shadowRadius
shadowLayer.name = "dropShadowLayer"
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
layer.insertSublayer(shadowLayer, at: 0)
}
func removeDropShadows() {
layer.sublayers?.removeAll { $0.name == "dropShadowLayer" }
}
func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) {
removeGradientLayer()
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 1)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
gradientLayer.position = center
gradientLayer.shouldRasterize = true
gradientLayer.rasterizationScale = UIScreen.main.scale
gradientLayer.cornerRadius = layer.cornerRadius
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.name = "gradientLayer"
layer.insertSublayer(gradientLayer, at: 0)
}
func removeGradientLayer() {
layer.sublayers?.removeAll { $0.name == "gradientLayer" }
}
}

View File

@ -7,7 +7,7 @@
import Foundation
public protocol EnumSubset<T>: RawRepresentable, CaseIterable {
public protocol EnumSubset<T>: RawRepresentable, CaseIterable, Valuing {
associatedtype T:RawRepresentable
var defaultValue: T { get }
}

View File

@ -9,10 +9,91 @@ import Foundation
/// Protocol used for a FormField object.
public protocol FormFieldable {
associatedtype ValueType = AnyHashable
/// Unique Id for the Form Field object within a Form.
var inputId: String? { get set }
/// Value for the Form Field.
var value: AnyHashable? { get set }
var value: ValueType? { get set }
}
/// Protocol for FormFieldable that require internal validation.
public protocol FormFieldInternalValidatable: FormFieldable {
/// Is there an internalError
var hasInternalError: Bool { get }
/// Internal Error Message that will show.
var internalErrorText: String? { get }
}
/// Struct that will execute the validation.
public protocol FormFieldValidatorable {
associatedtype FieldType: FormFieldable
/// FormFieldable to be validated.
var field: FieldType { get set }
/// Rules that will be applied against the FormFieldable
var rules: [AnyRule<FieldType.ValueType>] { get set }
/// Error Message that will show.
var errorMessage: String? { get }
/// Is the FormField valid.
var isValid: Bool { get }
/// Run the rules against the FormFieldable.
func validate()
}
/// Rule that will be executed against a specific ValueType.
public protocol Rule<ValueType> {
associatedtype ValueType
/// Determines if this rule valid for the value passed.
func isValid(value: ValueType?) -> Bool
/// Error Message to be show if the value is invalid.
var errorMessage: String { get }
}
/// Type Erased Rule for a specific ValueType.
public struct AnyRule<ValueType>: Rule {
private let _isValid: (ValueType?) -> Bool
public let errorMessage: String
public init<R: Rule>(_ rule: R) where R.ValueType == ValueType {
self._isValid = rule.isValid
self.errorMessage = rule.errorMessage
}
public func isValid(value: ValueType?) -> Bool {
return _isValid(value)
}
}
/// Generic Validator for a specific FormFieldable.
public class FormFieldValidator<Field:FormFieldable>: FormFieldValidatorable{
public var field: Field
public var rules: [AnyRule<Field.ValueType>]
public var errorMessages = [String]()
public var isValid: Bool = true
public init(field: Field, rules: [AnyRule<Field.ValueType>]) {
self.field = field
self.rules = rules
}
public var errorMessage: String? {
guard errorMessages.count > 0 else { return nil }
return errorMessages.joined(separator: "\r")
}
public func validate() {
errorMessages.removeAll()
for rule in rules {
if !rule.isValid(value: field.value) {
errorMessages.append(rule.errorMessage)
isValid = false
return
}
}
isValid = true
}
}

View File

@ -478,7 +478,159 @@ extension LayoutConstraintable {
}
}
//--------------------------------------------------
// MARK: - Center X Constraints
//--------------------------------------------------
extension LayoutConstraintable {
@discardableResult
/// Adds a centerXAnchor.
/// - Parameter constant: Constant size.
/// - Returns: Yourself.
public func pinCenterX(_ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
pinCenterX(nil, constant)
}
@discardableResult
/// Adds a centerXAnchor to a specific XAxisAnchor.
/// - Parameter anchor:The anchor in which to attach the centerXAnchor.
/// - constant: Constant size.
/// - Returns: Yourself.
public func pinCenterX(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
pinCenterX(anchor: anchor, constant: constant)
return self
}
@discardableResult
/// Adds a centerXAnchor to a specific XAxisAnchor passed in using a lessThanOrEqualTo Constraint
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
/// - constant: Constant size.
/// - Returns: Yourself.
public func pinCenterXLessThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
pinCenterXLessThanOrEqualTo(anchor: anchor, constant: constant)
return self
}
@discardableResult
/// Adds a centerXAnchor to a specific XAxisAnchor passed in using a greaterThanOrEqualTo Constraint
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
/// - constant: Constant size.
/// - Returns: Yourself.
public func pinCenterXGreaterThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self {
pinCenterXGreaterThanOrEqualTo(anchor: anchor, constant: constant)
return self
}
@discardableResult
/// Adds a centerXAnchor for the constant passed into the method.
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
/// - constant: Constant size.
/// - Returns: The Constraint that was created.
public func pinCenterX(anchor: NSLayoutXAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
let found: NSLayoutXAxisAnchor? = anchor ?? superview?.centerXAnchor
guard let found else { return nil }
return centerXAnchor.constraint(equalTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
@discardableResult
/// Adds a centerXAnchor with the constant passed in using a lessThanOrEqualTo Constraint.
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
/// - constant: Constant size.
/// - Returns: The Constraint that was created.
public func pinCenterXLessThanOrEqualTo(anchor: NSLayoutXAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
let found: NSLayoutXAxisAnchor? = anchor ?? superview?.centerXAnchor
guard let found else { return nil }
return centerXAnchor.constraint(lessThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
@discardableResult
/// Adds a centerXAnchor with the constant passed in using a greaterThanOrEqualTo Constraint.
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
/// - constant: Constant size.
/// - Returns: The Constraint that was created.
public func pinCenterXGreaterThanOrEqualTo(anchor: NSLayoutXAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
let found: NSLayoutXAxisAnchor? = anchor ?? superview?.centerXAnchor
guard let found else { return nil }
return centerXAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
}
//--------------------------------------------------
// MARK: - Center Y Constraints
//--------------------------------------------------
extension LayoutConstraintable {
@discardableResult
/// Adds a centerYAnchor.
/// - Parameter constant: Constant size.
/// - Returns: Yourself.
public func pinCenterY(_ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
pinCenterY(nil, constant)
}
@discardableResult
/// Adds a centerYAnchor to a specific YAxisAnchor.
/// - Parameter anchor:The anchor in which to attach the centerYAnchor.
/// - constant: Constant size.
/// - Returns: Yourself.
public func pinCenterY(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
pinCenterY(anchor: anchor, constant: constant)
return self
}
@discardableResult
/// Adds a centerYAnchor to a specific YAxisAnchor passed in using a lessThanOrEqualTo Constraint
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
/// - constant: Constant size.
/// - Returns: Yourself.
public func pinCenterYLessThanOrEqualTo(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
pinCenterYLessThanOrEqualTo(anchor: anchor, constant: constant)
return self
}
@discardableResult
/// Adds a centerYAnchor to a specific YAxisAnchor passed in using a greaterThanOrEqualTo Constraint
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
/// - constant: Constant size.
/// - Returns: Yourself.
public func pinCenterYGreaterThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self {
pinCenterXGreaterThanOrEqualTo(anchor: anchor, constant: constant)
return self
}
@discardableResult
/// Adds a centerYAnchor for the constant passed into the method.
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
/// - constant: Constant size.
/// - Returns: The Constraint that was created.
public func pinCenterY(anchor: NSLayoutYAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
let found: NSLayoutYAxisAnchor? = anchor ?? superview?.centerYAnchor
guard let found else { return nil }
return centerYAnchor.constraint(equalTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
@discardableResult
/// Adds a centerYAnchor with the constant passed in using a lessThanOrEqualTo Constraint.
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
/// - constant: Constant size.
/// - Returns: The Constraint that was created.
public func pinCenterYLessThanOrEqualTo(anchor: NSLayoutYAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
let found: NSLayoutYAxisAnchor? = anchor ?? superview?.centerYAnchor
guard let found else { return nil }
return centerYAnchor.constraint(lessThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
@discardableResult
/// Adds a centerYAnchor with the constant passed in using a greaterThanOrEqualTo Constraint.
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
/// - constant: Constant size.
/// - Returns: The Constraint that was created.
public func pinCenterYGreaterThanOrEqualTo(anchor: NSLayoutYAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
let found: NSLayoutYAxisAnchor? = anchor ?? superview?.centerYAnchor
guard let found else { return nil }
return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
}
//--------------------------------------------------
// MARK: - Implementations
//--------------------------------------------------

View File

@ -0,0 +1,13 @@
//
// Valuing.swift
// VDS
//
// Created by Matt Bruce on 3/1/24.
//
import Foundation
public protocol Valuing {
associatedtype ValueType
var value: ValueType { get }
}

View File

@ -1,3 +1,8 @@
1.0.55
----------------
- ONEAPP-6305 - BadgeIndicator - Finished Development
- ONEAPP-6679 - TileContainer - Finished Development
1.0.54
----------------
- CXTDT-518373 Accessibility Voiceover is reading “Still Loading” after waiting for a short time in all the screens.

View File

@ -210,6 +210,13 @@ extension TextStyle {
boldMicro
]
}
public static func convert(font: UIFont) -> TextStyle {
guard let found = allCases.first(where: { font.fontName == $0.fontFace.fontName && font.pointSize == $0.pointSize} ) else {
return TextStyle(rawValue: "Custom\(font.fontName)", fontFace: .custom(font), pointSize: font.pointSize)
}
return found
}
}
extension TextStyle {

View File

@ -0,0 +1,24 @@
//
// Clamping.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 05/03/24.
//
import Foundation
@propertyWrapper public struct Clamping<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
public init(range: ClosedRange<Value>) {
self.value = range.lowerBound
self.range = range
}
public var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}

View File

@ -0,0 +1,33 @@
//
// DropShadowConfiguration.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 05/03/24.
//
import Foundation
/**
DropShadowConfiguration confirms to DropShadowable where it has configurable properties required for drop shadow
*/
final class DropShadowConfiguration: DropShadowable, ObjectWithable {
typealias CGFloatConfigurationValue = SurfaceConfigurationValue<CGFloat>
typealias CGSizeConfigurationValue = SurfaceConfigurationValue<CGSize>
///Shadow Color configuration for light and dark surfaces
var shadowColorConfiguration: AnyColorable
///Shadow Opacity configuration for light and dark surfaces
var shadowOpacityConfiguration: CGFloatConfigurationValue
///Shadow Offset configuration for light and dark surfaces
var shadowOffsetConfiguration: CGSizeConfigurationValue
///Shadow Radius configuration for light and dark surfaces
var shadowRadiusConfiguration: CGFloatConfigurationValue
init(shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().eraseToAnyColorable(), shadowOpacity: CGFloatConfigurationValue = CGFloatConfigurationValue(1.0, 1.0), shadowOffset: CGSizeConfigurationValue = CGSizeConfigurationValue(.zero, .zero), shadowRadius: CGFloatConfigurationValue = CGFloatConfigurationValue(1.0, 1.0)) {
self.shadowColorConfiguration = shadowColorConfiguration
self.shadowOpacityConfiguration = shadowOpacity
self.shadowOffsetConfiguration = shadowOffset
self.shadowRadiusConfiguration = shadowRadius
}
}

View File

@ -0,0 +1,31 @@
//
// SurfaceConfigurationValue.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 05/03/24.
//
import Foundation
/**
SurfaceConfiguration is a type that holds the generic datatype for light surface & dark surface and returns the value based on the surface.
*/
struct SurfaceConfigurationValue<ValueType> {
var lightValue: ValueType
var darkValue: ValueType
public init(_ lightValue: ValueType, _ darkValue: ValueType) {
self.lightValue = lightValue
self.darkValue = darkValue
}
public init(value: ValueType) {
self.lightValue = value
self.darkValue = value
}
public func value(for object: Surfaceable) -> ValueType {
object.surface == .light ? lightValue : darkValue
}
}

View File

@ -21,9 +21,11 @@ Using the system allows designers and developers to collaborate more easily and
### Components
- ``Badge``
- ``BadgeIndicator``
- ``Breadcrumbs``
- ``Button``
- ``ButtonIcon``
- ``ButtonGroup``
- ``CarouselScrollbar``
- ``Checkbox``
- ``CheckboxItem``
- ``CheckboxGroup``
@ -33,6 +35,7 @@ Using the system allows designers and developers to collaborate more easily and
- ``Line``
- ``Loader``
- ``Notification``
- ``Pagination``
- ``RadioBoxItem``
- ``RadioBoxGroup``
- ``RadioButton``