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:
commit
5d19edbc6e
@ -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;
|
||||
|
||||
@ -132,4 +132,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
return true
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -86,4 +86,9 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
90
VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift
Normal file
90
VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
102
VDS/Components/Breadcrumbs/BreadcrumbItem.swift
Normal file
102
VDS/Components/Breadcrumbs/BreadcrumbItem.swift
Normal 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 ?? "")"
|
||||
}
|
||||
|
||||
}
|
||||
32
VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift
Normal file
32
VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
170
VDS/Components/Breadcrumbs/Breadcrumbs.swift
Normal file
170
VDS/Components/Breadcrumbs/Breadcrumbs.swift
Normal 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]
|
||||
}
|
||||
}
|
||||
36
VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt
Normal file
36
VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt
Normal 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
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
435
VDS/Components/CarouselScrollbar/CarouselScrollbar.swift
Normal file
435
VDS/Components/CarouselScrollbar/CarouselScrollbar.swift
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
245
VDS/Components/Pagination/Pagination.swift
Normal file
245
VDS/Components/Pagination/Pagination.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
111
VDS/Components/Pagination/PaginationButton.swift
Normal file
111
VDS/Components/Pagination/PaginationButton.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
VDS/Components/Pagination/PaginationCellItem.swift
Normal file
63
VDS/Components/Pagination/PaginationCellItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
34
VDS/Components/Pagination/PaginationChangeLog.txt
Normal file
34
VDS/Components/Pagination/PaginationChangeLog.txt
Normal 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.
|
||||
45
VDS/Components/Pagination/PaginationContainer.swift
Normal file
45
VDS/Components/Pagination/PaginationContainer.swift
Normal 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
|
||||
}
|
||||
}
|
||||
92
VDS/Components/Pagination/PaginationFlowLayout.swift
Normal file
92
VDS/Components/Pagination/PaginationFlowLayout.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
VDS/Components/TextFields/TextArea/TextAreaChangeLog.txt
Normal file
38
VDS/Components/TextFields/TextArea/TextAreaChangeLog.txt
Normal 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.
|
||||
151
VDS/Components/TextFields/TextArea/TextView.swift
Normal file
151
VDS/Components/TextFields/TextArea/TextView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
VDS/Components/Tilelet/TiletEyebrowModel.swift
Normal file
55
VDS/Components/Tilelet/TiletEyebrowModel.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
110
VDS/Components/Tilelet/TitleletChangeLog.txt
Normal file
110
VDS/Components/Tilelet/TitleletChangeLog.txt
Normal 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 Lockup’s 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
|
||||
@ -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)
|
||||
|
||||
@ -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 }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
39
VDS/Extensions/UIImage+Helper.swift
Normal file
39
VDS/Extensions/UIImage+Helper.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
54
VDS/Extensions/VDSLayout.swift
Normal file
54
VDS/Extensions/VDSLayout.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
12
VDS/Protocols/DefaultValuing.swift
Normal file
12
VDS/Protocols/DefaultValuing.swift
Normal 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 }
|
||||
}
|
||||
92
VDS/Protocols/DropShadowable.swift
Normal file
92
VDS/Protocols/DropShadowable.swift
Normal 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] }
|
||||
}
|
||||
@ -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" }
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
//--------------------------------------------------
|
||||
|
||||
13
VDS/Protocols/Valuing.swift
Normal file
13
VDS/Protocols/Valuing.swift
Normal 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 }
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
24
VDS/Utilities/Clamping.swift
Normal file
24
VDS/Utilities/Clamping.swift
Normal 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) }
|
||||
}
|
||||
}
|
||||
33
VDS/Utilities/DropShadowConfiguration.swift
Normal file
33
VDS/Utilities/DropShadowConfiguration.swift
Normal 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
|
||||
}
|
||||
}
|
||||
31
VDS/Utilities/SurfaceConfigurationValue.swift
Normal file
31
VDS/Utilities/SurfaceConfigurationValue.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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``
|
||||
|
||||
Loading…
Reference in New Issue
Block a user