diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 8f928491..6db040ae 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -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 = ""; }; + 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = ""; }; + 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = ""; }; + 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = ""; }; + 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; + 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = ""; }; 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; + 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; + 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = ""; }; 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; + 710607942B91A99500F2863F /* TitleletChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TitleletChangeLog.txt; sourceTree = ""; }; 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = ""; }; - 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dropshadowable.swift; sourceTree = ""; }; + 71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationContainer.swift; sourceTree = ""; }; + 71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiletEyebrowModel.swift; sourceTree = ""; }; + 71B23C2C2B91FA690027F7D9 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; + 71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PaginationChangeLog.txt; sourceTree = ""; }; + 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowable.swift; sourceTree = ""; }; 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = NotificationChangeLog.txt; sourceTree = ""; }; + 71FC86D92B96F44C00700965 /* PaginationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationButton.swift; sourceTree = ""; }; + 71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationCellItem.swift; sourceTree = ""; }; + 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceConfigurationValue.swift; sourceTree = ""; }; + 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowConfiguration.swift; sourceTree = ""; }; + 71FC86E12B97483000700965 /* Clamping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clamping.swift; sourceTree = ""; }; + 71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationFlowLayout.swift; sourceTree = ""; }; EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = ""; }; EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = ""; }; EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = ""; }; @@ -230,6 +272,7 @@ EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = ""; }; EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = ""; }; + EA6F330D2B911E9000BACAB9 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; @@ -258,6 +301,8 @@ EAA5EEDF28F49DB3003B3210 /* Colorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colorable.swift; sourceTree = ""; }; EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGDS-Light.otf"; sourceTree = ""; }; EAA7456B2AB23E2000C1841F /* TooltipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipModel.swift; sourceTree = ""; }; + EAACB8972B92706F006A3869 /* DefaultValuing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultValuing.swift; sourceTree = ""; }; + EAACB8992B927108006A3869 /* Valuing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valuing.swift; sourceTree = ""; }; EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; EAB1D2CC28ABE76000DAE764 /* Withable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Withable.swift; sourceTree = ""; }; EAB1D2CE28ABEF2B00DAE764 /* Typography+Base.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typography+Base.swift"; sourceTree = ""; }; @@ -287,6 +332,7 @@ EAD068912A560B65002E3A2D /* LoaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderViewController.swift; sourceTree = ""; }; EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderLaunchable.swift; sourceTree = ""; }; EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = ""; }; + EAE785302BA0A438009428EA /* UIImage+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Helper.swift"; sourceTree = ""; }; EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BadgeChangeLog.txt; sourceTree = ""; }; EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonChangeLog.txt; sourceTree = ""; }; EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LineChangeLog.txt; sourceTree = ""; }; @@ -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 = ""; + }; + 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */ = { + isa = PBXGroup; + children = ( + 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */, + 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */, + ); + path = CarouselScrollbar; + sourceTree = ""; + }; 445BA07629C07ABA0036A7C5 /* Notification */ = { isa = PBXGroup; children = ( @@ -370,6 +437,19 @@ path = Button; sourceTree = ""; }; + 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 = ""; + }; 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 = ""; @@ -565,6 +651,9 @@ isa = PBXGroup; children = ( EA3361BC288B2C760071C351 /* TypeAlias.swift */, + 71FC86E12B97483000700965 /* Clamping.swift */, + 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */, + 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */, ); path = Utilities; sourceTree = ""; @@ -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 = ""; @@ -701,6 +792,8 @@ isa = PBXGroup; children = ( EA985C22296E033A00F2FF2E /* TextArea.swift */, + EA6F330D2B911E9000BACAB9 /* TextView.swift */, + 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */, ); path = TextArea; sourceTree = ""; @@ -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; diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index e4ba8cde..cc2246aa 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -132,4 +132,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { return true } + open override func layoutSubviews() { + super.layoutSubviews() + setNeedsUpdate() + } } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 353b46e7..1900c2c5 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -101,7 +101,6 @@ open class SelectorItemBase: 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: 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() } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index 89e5ce4b..a807c25c 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -86,4 +86,9 @@ open class View: UIView, ViewProtocol, UserInfoable { isEnabled = true } + open override func layoutSubviews() { + super.layoutSubviews() + setNeedsUpdate() + } + } diff --git a/VDS/Classes/SelfSizingCollectionView.swift b/VDS/Classes/SelfSizingCollectionView.swift index c50082c4..ad5d5661 100644 --- a/VDS/Classes/SelfSizingCollectionView.swift +++ b/VDS/Classes/SelfSizingCollectionView.swift @@ -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(.zero) + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var contentSizePublisher: AnyPublisher { + 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) } } - } } } diff --git a/VDS/Components/Badge/Badge.swift b/VDS/Components/Badge/Badge.swift index d91209c1..7e8ca443 100644 --- a/VDS/Components/Badge/Badge.swift +++ b/VDS/Components/Badge/Badge.swift @@ -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. diff --git a/VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift b/VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift new file mode 100644 index 00000000..08a823fa --- /dev/null +++ b/VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift @@ -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() + } +} + diff --git a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift new file mode 100644 index 00000000..a7b9bef4 --- /dev/null +++ b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift @@ -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 ?? "")" + } + +} diff --git a/VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift b/VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift new file mode 100644 index 00000000..75c97c86 --- /dev/null +++ b/VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift @@ -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 + } + } +} diff --git a/VDS/Components/Breadcrumbs/Breadcrumbs.swift b/VDS/Components/Breadcrumbs/Breadcrumbs.swift new file mode 100644 index 00000000..63084192 --- /dev/null +++ b/VDS/Components/Breadcrumbs/Breadcrumbs.swift @@ -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] + } +} diff --git a/VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt b/VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt new file mode 100644 index 00000000..3fae7754 --- /dev/null +++ b/VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt @@ -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 diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 58950e94..b0694848 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -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 { diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift index 825b6cb9..d9026280 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift @@ -98,6 +98,8 @@ open class ButtonGroup: View { buttons.forEach { $0.surface = surface } } } + + open var contentSizePublisher: AnyPublisher { 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 { diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroupConstants.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroupConstants.swift index 150b8928..3e02465c 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroupConstants.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroupConstants.swift @@ -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 { diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift index f4ccf276..5d3a339f 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift @@ -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) + } } diff --git a/VDS/Components/Buttons/TextLink/TextLink.swift b/VDS/Components/Buttons/TextLink/TextLink.swift index 0da7ae0b..9cff288c 100644 --- a/VDS/Components/Buttons/TextLink/TextLink.swift +++ b/VDS/Components/Buttons/TextLink/TextLink.swift @@ -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) diff --git a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift index e3900fe6..52ad9b82 100644 --- a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift +++ b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift @@ -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 { diff --git a/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift b/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift new file mode 100644 index 00000000..fa00b715 --- /dev/null +++ b/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift @@ -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() + 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() + 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() + 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() + 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() + 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() + } + }) + } +} diff --git a/VDS/Components/CarouselScrollbar/CarouselScrollbarChangeLog.txt b/VDS/Components/CarouselScrollbar/CarouselScrollbarChangeLog.txt new file mode 100644 index 00000000..68179e36 --- /dev/null +++ b/VDS/Components/CarouselScrollbar/CarouselScrollbarChangeLog.txt @@ -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. + diff --git a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift index c1215cbd..1267000a 100644 --- a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift +++ b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift @@ -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() + } } //-------------------------------------------------- diff --git a/VDS/Components/Icon/Icon.swift b/VDS/Components/Icon/Icon.swift index 587601c8..09c1ffec 100644 --- a/VDS/Components/Icon/Icon.swift +++ b/VDS/Components/Icon/Icon.swift @@ -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) + } +} diff --git a/VDS/Components/Icon/IconName.swift b/VDS/Components/Icon/IconName.swift index c600eb82..825d7e30 100644 --- a/VDS/Components/Icon/IconName.swift +++ b/VDS/Components/Icon/IconName.swift @@ -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") diff --git a/VDS/Components/Label/Attributes/ActionLabelAttribute.swift b/VDS/Components/Label/Attributes/ActionLabelAttribute.swift index acd28f91..6d7a1034 100644 --- a/VDS/Components/Label/Attributes/ActionLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/ActionLabelAttribute.swift @@ -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) } diff --git a/VDS/Components/Label/Attributes/AnyLabelAttribute.swift b/VDS/Components/Label/Attributes/AnyLabelAttribute.swift index b914c912..50e76ba7 100644 --- a/VDS/Components/Label/Attributes/AnyLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/AnyLabelAttribute.swift @@ -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) } diff --git a/VDS/Components/Label/Attributes/AttachmentLabelAttributeModel.swift b/VDS/Components/Label/Attributes/AttachmentLabelAttributeModel.swift index 5de6f4b2..c4f9bd79 100644 --- a/VDS/Components/Label/Attributes/AttachmentLabelAttributeModel.swift +++ b/VDS/Components/Label/Attributes/AttachmentLabelAttributeModel.swift @@ -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() diff --git a/VDS/Components/Label/Attributes/ColorLabelAttribute.swift b/VDS/Components/Label/Attributes/ColorLabelAttribute.swift index 354fbc93..50229ab0 100644 --- a/VDS/Components/Label/Attributes/ColorLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/ColorLabelAttribute.swift @@ -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) diff --git a/VDS/Components/Label/Attributes/LabelAttributeModel.swift b/VDS/Components/Label/Attributes/LabelAttributeModel.swift index 62e1bbd8..01dfec08 100644 --- a/VDS/Components/Label/Attributes/LabelAttributeModel.swift +++ b/VDS/Components/Label/Attributes/LabelAttributeModel.swift @@ -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) } diff --git a/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift b/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift index 93650f2b..ba4c97ac 100644 --- a/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift @@ -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) } diff --git a/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift b/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift index ae3ea1e5..a161d580 100644 --- a/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift @@ -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 { diff --git a/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift b/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift index 7d9a49a0..76efcec7 100644 --- a/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift @@ -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 diff --git a/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift b/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift index 0ca4e2f9..a2ad403e 100644 --- a/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift @@ -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) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 64cab6e3..df67d431 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -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 { } } } + + diff --git a/VDS/Components/Loader/Loader.swift b/VDS/Components/Loader/Loader.swift index 21fb6778..fb2f5371 100644 --- a/VDS/Components/Loader/Loader.swift +++ b/VDS/Components/Loader/Loader.swift @@ -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() { diff --git a/VDS/Components/Notification/Notification.swift b/VDS/Components/Notification/Notification.swift index ce6fec45..2d41f4a5 100644 --- a/VDS/Components/Notification/Notification.swift +++ b/VDS/Components/Notification/Notification.swift @@ -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 } } diff --git a/VDS/Components/Pagination/Pagination.swift b/VDS/Components/Pagination/Pagination.swift new file mode 100644 index 00000000..11a5840d --- /dev/null +++ b/VDS/Components/Pagination/Pagination.swift @@ -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) + } +} diff --git a/VDS/Components/Pagination/PaginationButton.swift b/VDS/Components/Pagination/PaginationButton.swift new file mode 100644 index 00000000..9e0eeb47 --- /dev/null +++ b/VDS/Components/Pagination/PaginationButton.swift @@ -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) + } + } + } +} diff --git a/VDS/Components/Pagination/PaginationCellItem.swift b/VDS/Components/Pagination/PaginationCellItem.swift new file mode 100644 index 00000000..c2a1551b --- /dev/null +++ b/VDS/Components/Pagination/PaginationCellItem.swift @@ -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) + } +} diff --git a/VDS/Components/Pagination/PaginationChangeLog.txt b/VDS/Components/Pagination/PaginationChangeLog.txt new file mode 100644 index 00000000..66f0ac14 --- /dev/null +++ b/VDS/Components/Pagination/PaginationChangeLog.txt @@ -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. diff --git a/VDS/Components/Pagination/PaginationContainer.swift b/VDS/Components/Pagination/PaginationContainer.swift new file mode 100644 index 00000000..2172e32a --- /dev/null +++ b/VDS/Components/Pagination/PaginationContainer.swift @@ -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 + } +} diff --git a/VDS/Components/Pagination/PaginationFlowLayout.swift b/VDS/Components/Pagination/PaginationFlowLayout.swift new file mode 100644 index 00000000..07d25193 --- /dev/null +++ b/VDS/Components/Pagination/PaginationFlowLayout.swift @@ -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.. [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) + } +} diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index d1823fd9..ca32516f 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -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() } diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 5691ed34..225b3f98 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -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] } } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index b86405da..a1c247e0 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -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 diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index e78d8771..3e545410 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -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 { diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 39383291..ee4db48e 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -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