From 0673a876d1e9f714e0ed4b899f2ff7de986dce86 Mon Sep 17 00:00:00 2001 From: vasavk Date: Wed, 29 May 2024 16:06:51 +0530 Subject: [PATCH 01/31] Digital ACT-191 ONEAPP-7013 story: added new page --- VDS.xcodeproj/project.pbxproj | 16 +++++++++ VDS/Components/Carousel/Carousel.swift | 33 +++++++++++++++++++ VDS/Components/Carousel/CarouselChangeLog.txt | 15 +++++++++ 3 files changed, 64 insertions(+) create mode 100644 VDS/Components/Carousel/Carousel.swift create mode 100644 VDS/Components/Carousel/CarouselChangeLog.txt diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 8f6878a0..f808e999 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; + 18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; }; + 18AE87542C06FE610075F181 /* CarouselChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; @@ -237,6 +239,8 @@ 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; + 18AE874F2C06FDA60075F181 /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = ""; }; + 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; @@ -493,6 +497,15 @@ path = Breadcrumbs; sourceTree = ""; }; + 18AE874E2C06FD610075F181 /* Carousel */ = { + isa = PBXGroup; + children = ( + 18AE874F2C06FDA60075F181 /* Carousel.swift */, + 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, + ); + path = Carousel; + sourceTree = ""; + }; 445BA07629C07ABA0036A7C5 /* Notification */ = { isa = PBXGroup; children = ( @@ -649,6 +662,7 @@ 18A65A002B96E7E1006602CC /* Breadcrumbs */, EA0FC2BE2912D18200DF80B4 /* Buttons */, 18A3F1202BD8F5DE00498E4A /* Calendar */, + 18AE874E2C06FD610075F181 /* Carousel */, 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */, EAF7F092289985E200B287F5 /* Checkbox */, EAC58C1F2BF127F000BA39FA /* DatePicker */, @@ -1164,6 +1178,7 @@ EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */, EAEEEC9A2B1F8E4400531FC2 /* TextLinkChangeLog.txt in Resources */, 1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */, + 18AE87542C06FE610075F181 /* CarouselChangeLog.txt in Resources */, EAEEECAF2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt in Resources */, EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */, EAEEEC9E2B1F8F7700531FC2 /* ButtonGroupChangeLog.txt in Resources */, @@ -1321,6 +1336,7 @@ EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */, EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, + 18AE87502C06FDA60075F181 /* Carousel.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */, EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift new file mode 100644 index 00000000..84bfd340 --- /dev/null +++ b/VDS/Components/Carousel/Carousel.swift @@ -0,0 +1,33 @@ +// +// Carousel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 29/05/24. +// + +import Foundation +import UIKit +import VDSTokens +import Combine + +/// A carousel is a collection of related content in a row that a customer can navigate through horizontally. +/// Use this component to show content that is supplementary, not essential for task completion. +@objc(VDSCarousel) +open class Carousel: 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) + } + +} diff --git a/VDS/Components/Carousel/CarouselChangeLog.txt b/VDS/Components/Carousel/CarouselChangeLog.txt new file mode 100644 index 00000000..a1fd0f4f --- /dev/null +++ b/VDS/Components/Carousel/CarouselChangeLog.txt @@ -0,0 +1,15 @@ +MM/DD/YYYY +---------------- + +06/22/2023 +---------------- +- Initial Beta Release + +10/02/2023 +---------------- +- Removed (Beta) from header. Removed deprecated sections and “New” badge from Kind section. + +11/20/2023 +---------------- +- Updated visuals to reflect new corner radius value - 12px +- Updated focus border corner radius to 14px From 57a0486b3fc81f35c185a15c181df15941122fe7 Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 4 Jun 2024 09:30:41 +0530 Subject: [PATCH 02/31] Digital ACT-191 ONEAPP-7016 story: added properties, added Carousel to VDS.md --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Components/Carousel/Carousel.swift | 278 ++++++++++++++++++ .../Carousel/CarouselSlotAlignmentModel.swift | 25 ++ VDS/VDS.docc/VDS.md | 1 + 4 files changed, 308 insertions(+) create mode 100644 VDS/Components/Carousel/CarouselSlotAlignmentModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index f808e999..27693abe 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; }; 18AE87542C06FE610075F181 /* CarouselChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */; }; + 18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; @@ -241,6 +242,7 @@ 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; 18AE874F2C06FDA60075F181 /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = ""; }; 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = ""; }; + 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotAlignmentModel.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; @@ -501,6 +503,7 @@ isa = PBXGroup; children = ( 18AE874F2C06FDA60075F181 /* Carousel.swift */, + 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */, 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, ); path = Carousel; @@ -1300,6 +1303,7 @@ EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */, 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */, EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */, + 18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */, 71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */, EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */, EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 84bfd340..a90e4e4b 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -30,4 +30,282 @@ open class Carousel: View { super.init(coder: coder) } + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Aspect-ratio options for tilelet in the carousel. If 'none' is passed, the tilelet will take the height of the tallest item in the carousel. + open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } + + /// Data used to render tilelets in the carousel. + open var data: Array? = [] { didSet { setNeedsUpdate() } } + + /// Space between each tile. The default value will be 24px in tablet and 12px in mobile. + open var gutter: Gutter? { + get { return _gutter } + set { + if let newValue { + _gutter = newValue + } else { + _gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX + } + setNeedsUpdate() + } + } + + /// The amount of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. + open var layout: Layout? { + get { return _layout } + set { + if let newValue { + _layout = newValue + } else { + _layout = UIDevice.isIPad ? .threeUP : .oneUP + } + setNeedsUpdate() + } + } + + /// A callback when moving the carousel. Returns event object and selectedGroupIndex. + open var onChange: ((Int) -> Void)? { // TO DO: return object and index + get { nil } + set { + onChangeCancellable?.cancel() + if let newValue { + onChangeCancellable = onChangePublisher + .sink { c in + newValue(c) + } + } + } + } + + // TO DO: pagination + /// Config object for pagination. + /// Custom data type for pagination prop in this component. + + /// If provided, will determine the conditions to render the pagination arrows. + open var paginationDisplay: PaginationDisplay? { + get { return _paginationDisplay } + set { + if let newValue { + _paginationDisplay = newValue + } else { + _paginationDisplay = .none + } + setNeedsUpdate() + } + } + + /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. + /// The default value will be 12px in tablet and 8px in mobile. + open var paginationInset: CGFloat? { + get { return _paginationInset } + set { + if let newValue { + _paginationInset = newValue + } else { + _paginationInset = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X + } + setNeedsUpdate() + } + } + + /// Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. + open var peek: Peek? { + get { return _peek } + set { + if let newValue { + _peek = newValue + } else { + _peek = .none + } + setNeedsUpdate() + } + } + + // TO DO: renderItem + /// Render item function. This will pass the data array and expects a react component in return. +// open var renderItem: ([] -> Any)? { // TO DO: return object and index +// get { nil } +// set { +// onScrollCancellable?.cancel() +// if let newValue { +// onScrollCancellable = onScrollPublisher +// .sink { c in +// newValue(c) +// } +// } +// } +// } +// open var renderItem: ([] -> Void)? {} + + /// The initial visible slide's index in the carousel. + open var selectedIndex: Int? { didSet { setNeedsUpdate() } } + + /// If provided, will set the alignment for slot content when the slots has different heights. + open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } + + /// + open var hidePaginationBorder: Bool = false { didSet { setNeedsUpdate() } } + + /// viewport + /// viewportOverride + /// viewportOverrideObjectType + /// Custom data type for viewportOverride prop in this component. + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + // Sizes are from InVision design specs. + internal var containerSize: CGSize { CGSize(width: 320, height: 44) } + internal var containerView = View().with { + $0.clipsToBounds = true + } + + /// Previous button to show previous slide. + private let previousButton: PaginationButton = .init(type: .previous) + + /// Next button to show next slide. + private let nextButton: PaginationButton = .init(type: .next) + + /// A publisher for when the scrubber position changes. Passes parameters (position). + open var onChangePublisher = PassthroughSubject() + private var onChangeCancellable: AnyCancellable? + + /// A publisher for when the carousel moves. Passes parameters (data). +// open var onScrollPublisher = PassthroughSubject, Never>() +// private var onScrollCancellable: AnyCancellable? + + internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP + internal var _paginationDisplay: PaginationDisplay = .none + internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X + internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX + internal var _peek: Peek = .none + + private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } +// private var minPaginationInset: CGFloat { +// return UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X +// } + + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- + /// Enum used to describe the aspect ratios used for this component. + public enum AspectRatio: String, CaseIterable { + case ratio1x1 = "1:1" + case ratio3x4 = "3:4" + case ratio4x3 = "4:3" + case ratio2x3 = "2:3" + case ratio3x2 = "3:2" + case ratio9x16 = "9:16" + case ratio16x9 = "16:9" + case ratio1x2 = "1:2" + case ratio2x1 = "2:1" + case none + } + + /// Enum used to describe the number of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. + public enum Layout: String, CaseIterable { + case oneUP = "1UP" + case twoUP = "2UP" + case threeUP = "3UP" + case fourUP = "4UP" + case fiveUP = "5UP" + case sixUP = "6UP" + + var value: Int { + switch self { + case .oneUP: + 1 + case .twoUP: + 2 + case .threeUP: + 3 + case .fourUP: + 4 + case .fiveUP: + 5 + case .sixUP: + 6 + } + } + } + + /// Enum used to describe the number of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. + public enum Gutter: String, CaseIterable { + case twelvePX = "12px" + case twentyFourPX = "24px" + + var value: CGFloat { + switch self { + case .twelvePX: + VDSLayout.space12X + case .twentyFourPX: + VDSLayout.space24X + } + } + } + + /// Enum used to describe the pagination display for this component. + public enum PaginationDisplay: String, CaseIterable { + case onHover, persistent, none + } + + /// Enum used to describe the peek for this component. Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. + public enum Peek: String, CaseIterable { + case standard, minimum, none + } + + /// Enum used to describe the pagination kind for pagination button icons. + public enum PaginationKind: String, CaseIterable { + case ghost, lowContrast, highContrast + } // TO DO: it should be used as pagination properties, validate desc stmt. and API - passing data is different. + + // TO DO: move to model class + /// Enum used to describe the vertical of slotAlignment. + public enum Vertical: String, CaseIterable { + case top, middle, bottom + } + + /// Enum used to describe the horizontal of slotAlignment. + public enum Horizontal: String, CaseIterable { + case left, center, right + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + open override func setup() { + super.setup() + isAccessibilityElement = false + + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) +// .width(containerSize.width) + + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + } + + open override func updateView() { + super.updateView() + } + + open override func reset() { +// for subview in subviews { +// for recognizer in subview.gestureRecognizers ?? [] { +// subview.removeGestureRecognizer(recognizer) +// } +// } + super.reset() + } } diff --git a/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift b/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift new file mode 100644 index 00000000..5a8ec661 --- /dev/null +++ b/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift @@ -0,0 +1,25 @@ +// +// CarouselSlotAlignmentModel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 31/05/24. +// + +import Foundation + +/// Custom data type for slotAlignment prop for 'Carousel' component. +extension Carousel { + public struct CarouselSlotAlignmentModel { + + /// Text that shown to an indicator for legend + public var vertical: String + + /// Date to an indicator + public var horizontal: String + + public init(vertical: String, horizontal: String) { + self.vertical = vertical + self.horizontal = horizontal + } + } +} diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index d145ea0a..6582ce4a 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -26,6 +26,7 @@ Using the system allows designers and developers to collaborate more easily and - ``ButtonIcon`` - ``ButtonGroup`` - ``CalendarBase`` +- ``Carousel`` - ``CarouselScrollbar`` - ``Checkbox`` - ``CheckboxItem`` From bc35186bf65dcbb4ed9a102a3239528e13b8a987 Mon Sep 17 00:00:00 2001 From: vasavk Date: Sun, 9 Jun 2024 11:02:18 +0530 Subject: [PATCH 03/31] Digital ACT-191 ONEAPP-7013 story: added pagination model --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Components/Carousel/Carousel.swift | 221 +++++++++--------- .../Carousel/CarouselPaginationModel.swift | 40 ++++ 3 files changed, 160 insertions(+), 105 deletions(-) create mode 100644 VDS/Components/Carousel/CarouselPaginationModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 27693abe..80abac94 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 18AE87542C06FE610075F181 /* CarouselChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */; }; 18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; + 18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; @@ -244,6 +245,7 @@ 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = ""; }; 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotAlignmentModel.swift; sourceTree = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; + 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPaginationModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; @@ -503,6 +505,7 @@ isa = PBXGroup; children = ( 18AE874F2C06FDA60075F181 /* Carousel.swift */, + 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */, 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */, 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, ); @@ -1293,6 +1296,7 @@ EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */, EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */, EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */, + 18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */, EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */, EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */, 71FC86DA2B96F44C00700965 /* PaginationButton.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index a90e4e4b..b7f045df 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -37,30 +37,22 @@ open class Carousel: View { open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } /// Data used to render tilelets in the carousel. - open var data: Array? = [] { didSet { setNeedsUpdate() } } + open var data: [Any] = [] { didSet { setNeedsUpdate() } } /// Space between each tile. The default value will be 24px in tablet and 12px in mobile. - open var gutter: Gutter? { + open var gutter: Gutter { get { return _gutter } set { - if let newValue { - _gutter = newValue - } else { - _gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX - } + _gutter = newValue setNeedsUpdate() } } /// The amount of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. - open var layout: Layout? { + open var layout: Layout { get { return _layout } set { - if let newValue { - _layout = newValue - } else { - _layout = UIDevice.isIPad ? .threeUP : .oneUP - } + _layout = newValue setNeedsUpdate() } } @@ -78,114 +70,49 @@ open class Carousel: View { } } } - - // TO DO: pagination + /// Config object for pagination. - /// Custom data type for pagination prop in this component. + open var pagination: CarouselPaginationModel { + get { return _pagination } + set { + _pagination = newValue + setNeedsUpdate() + } + } /// If provided, will determine the conditions to render the pagination arrows. - open var paginationDisplay: PaginationDisplay? { + open var paginationDisplay: PaginationDisplay { get { return _paginationDisplay } set { - if let newValue { - _paginationDisplay = newValue - } else { - _paginationDisplay = .none - } + _paginationDisplay = newValue setNeedsUpdate() } } /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. /// The default value will be 12px in tablet and 8px in mobile. - open var paginationInset: CGFloat? { + open var paginationInset: CGFloat { get { return _paginationInset } set { - if let newValue { - _paginationInset = newValue - } else { - _paginationInset = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X - } + _paginationInset = newValue setNeedsUpdate() } } /// Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. - open var peek: Peek? { + open var peek: Peek { get { return _peek } set { - if let newValue { - _peek = newValue - } else { - _peek = .none - } + _peek = newValue setNeedsUpdate() } } - - // TO DO: renderItem - /// Render item function. This will pass the data array and expects a react component in return. -// open var renderItem: ([] -> Any)? { // TO DO: return object and index -// get { nil } -// set { -// onScrollCancellable?.cancel() -// if let newValue { -// onScrollCancellable = onScrollPublisher -// .sink { c in -// newValue(c) -// } -// } -// } -// } -// open var renderItem: ([] -> Void)? {} /// The initial visible slide's index in the carousel. open var selectedIndex: Int? { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. - open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } - - /// - open var hidePaginationBorder: Bool = false { didSet { setNeedsUpdate() } } - - /// viewport - /// viewportOverride - /// viewportOverrideObjectType - /// Custom data type for viewportOverride prop in this component. - - //-------------------------------------------------- - // MARK: - Private Properties - //-------------------------------------------------- - // Sizes are from InVision design specs. - internal var containerSize: CGSize { CGSize(width: 320, height: 44) } - internal var containerView = View().with { - $0.clipsToBounds = true - } - - /// Previous button to show previous slide. - private let previousButton: PaginationButton = .init(type: .previous) - - /// Next button to show next slide. - private let nextButton: PaginationButton = .init(type: .next) - - /// A publisher for when the scrubber position changes. Passes parameters (position). - open var onChangePublisher = PassthroughSubject() - private var onChangeCancellable: AnyCancellable? - - /// A publisher for when the carousel moves. Passes parameters (data). -// open var onScrollPublisher = PassthroughSubject, Never>() -// private var onScrollCancellable: AnyCancellable? - - internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP - internal var _paginationDisplay: PaginationDisplay = .none - internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X - internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX - internal var _peek: Peek = .none - - private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } -// private var minPaginationInset: CGFloat { -// return UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X -// } + open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Enums @@ -255,12 +182,7 @@ open class Carousel: View { public enum Peek: String, CaseIterable { case standard, minimum, none } - - /// Enum used to describe the pagination kind for pagination button icons. - public enum PaginationKind: String, CaseIterable { - case ghost, lowContrast, highContrast - } // TO DO: it should be used as pagination properties, validate desc stmt. and API - passing data is different. - + // TO DO: move to model class /// Enum used to describe the vertical of slotAlignment. public enum Vertical: String, CaseIterable { @@ -271,6 +193,61 @@ open class Carousel: View { public enum Horizontal: String, CaseIterable { case left, center, right } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + // Sizes are from InVision design specs. + internal var containerSize: CGSize { CGSize(width: 320, height: 44) } + + private let contentStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.spacing = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X + $0.backgroundColor = .clear + } + + internal var carouselScrollBar = CarouselScrollbar().with { + $0.layout = UIDevice.isIPad ? .threeUP : .oneUP + $0.position = 0 + $0.backgroundColor = .clear + } + + internal var containerView = View().with { + $0.clipsToBounds = true + $0.backgroundColor = .clear + } + + internal var scrollContainerView = View().with { + $0.clipsToBounds = true + $0.backgroundColor = .clear + } + + private var scrollView = UIScrollView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + } + + /// A publisher for when the scrubber position changes. Passes parameters (position). + open var onChangePublisher = PassthroughSubject() + private var onChangeCancellable: AnyCancellable? + + internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP + internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) + internal var _paginationDisplay: PaginationDisplay = .none + internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X + internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX + internal var _peek: Peek = .none + internal var _numberOfSlides: Int = 1 + + private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } + + private var containerStackHeightConstraint: NSLayoutConstraint? + private var containerViewHeightConstraint: NSLayoutConstraint? + + // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. + let space = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X //-------------------------------------------------- // MARK: - Lifecycle @@ -282,22 +259,56 @@ open class Carousel: View { open override func setup() { super.setup() isAccessibilityElement = false - + + // add containerView addSubview(containerView) containerView .pinTop() .pinBottom() - .pinLeadingGreaterThanOrEqualTo() - .pinTrailingLessThanOrEqualTo() - .height(containerSize.height) -// .width(containerSize.width) - + .pinLeading() + .pinTrailing() + .heightGreaterThanEqualTo(containerSize.height) containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // add content stackview + containerView.addSubview(contentStackView) + + // add scrollview + scrollContainerView.addSubview(scrollView) + + // add pagination button icons + scrollContainerView.addSubview(pagination.previousButton) + pagination.previousButton + .pinLeading(paginationInset) + .pinCenterY() + + scrollContainerView.addSubview(pagination.nextButton) + pagination.nextButton + .pinTrailing(paginationInset) + .pinCenterY() + // add scroll container view & carousel scrollbar + contentStackView.addArrangedSubview(scrollContainerView) + contentStackView.addArrangedSubview(carouselScrollBar) + contentStackView.setCustomSpacing(space, after: scrollContainerView) + contentStackView + .pinTop() + .pinBottom() + .pinLeading() + .pinTrailing() + .heightGreaterThanEqualTo(space+containerSize.height) + contentStackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } open override func updateView() { + containerViewHeightConstraint?.isActive = false super.updateView() + containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 200) + containerViewHeightConstraint?.isActive = true + + pagination.previousButton.isHidden = (paginationDisplay == .none) + pagination.nextButton.isHidden = (paginationDisplay == .none) + layoutIfNeeded() } open override func reset() { diff --git a/VDS/Components/Carousel/CarouselPaginationModel.swift b/VDS/Components/Carousel/CarouselPaginationModel.swift new file mode 100644 index 00000000..0abeeded --- /dev/null +++ b/VDS/Components/Carousel/CarouselPaginationModel.swift @@ -0,0 +1,40 @@ +// +// CarouselPaginationModel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 06/06/24. +// + +import Foundation +import UIKit + +/// Custom data type for pagination prop for 'Carousel' component. +extension Carousel { + public struct CarouselPaginationModel { + + /// Previous button to show previous slide. + public var previousButton = ButtonIcon().with { + $0.kind = .lowContrast + $0.iconName = .leftCaret + $0.iconOffset = .init(x: -2, y: 0) + $0.icon.size = UIDevice.isIPad ? .small : .xsmall + $0.size = UIDevice.isIPad ? .large : .small + } + + /// Next button to show next slide. + public var nextButton = ButtonIcon().with { + $0.kind = .lowContrast + $0.iconName = .rightCaret + $0.iconOffset = .init(x: 2, y: 0) + $0.icon.size = UIDevice.isIPad ? .small : .xsmall + $0.size = UIDevice.isIPad ? .large : .small + } + + public init(kind: ButtonIcon.Kind, floating: Bool) { + previousButton.kind = kind + nextButton.kind = kind + previousButton.floating = floating + nextButton.floating = floating + } + } +} From 0fc8c5c530e8c466b094cce26fe994f9342e36dd Mon Sep 17 00:00:00 2001 From: vasavk Date: Sun, 9 Jun 2024 11:25:39 +0530 Subject: [PATCH 04/31] Digital ACT-191 ONEAPP-7013 story: updating pagination buttons while changing pagination properties --- VDS/Components/Carousel/Carousel.swift | 36 +++++++++++++++---- .../Carousel/CarouselPaginationModel.swift | 28 ++++----------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index b7f045df..4007f5fe 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -228,7 +228,25 @@ open class Carousel: View { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear } - + + /// Previous button to show previous slide. + private var previousButton = ButtonIcon().with { + $0.kind = .lowContrast + $0.iconName = .leftCaret + $0.iconOffset = .init(x: -2, y: 0) + $0.icon.size = UIDevice.isIPad ? .small : .xsmall + $0.size = UIDevice.isIPad ? .large : .small + } + + /// Next button to show next slide. + private var nextButton = ButtonIcon().with { + $0.kind = .lowContrast + $0.iconName = .rightCaret + $0.iconOffset = .init(x: 2, y: 0) + $0.icon.size = UIDevice.isIPad ? .small : .xsmall + $0.size = UIDevice.isIPad ? .large : .small + } + /// A publisher for when the scrubber position changes. Passes parameters (position). open var onChangePublisher = PassthroughSubject() private var onChangeCancellable: AnyCancellable? @@ -277,13 +295,13 @@ open class Carousel: View { scrollContainerView.addSubview(scrollView) // add pagination button icons - scrollContainerView.addSubview(pagination.previousButton) - pagination.previousButton + scrollContainerView.addSubview(previousButton) + previousButton .pinLeading(paginationInset) .pinCenterY() - scrollContainerView.addSubview(pagination.nextButton) - pagination.nextButton + scrollContainerView.addSubview(nextButton) + nextButton .pinTrailing(paginationInset) .pinCenterY() @@ -306,8 +324,12 @@ open class Carousel: View { containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 200) containerViewHeightConstraint?.isActive = true - pagination.previousButton.isHidden = (paginationDisplay == .none) - pagination.nextButton.isHidden = (paginationDisplay == .none) + previousButton.isHidden = (paginationDisplay == .none) + nextButton.isHidden = (paginationDisplay == .none) + previousButton.kind = pagination.kind + previousButton.floating = pagination.floating + nextButton.kind = pagination.kind + nextButton.floating = pagination.floating layoutIfNeeded() } diff --git a/VDS/Components/Carousel/CarouselPaginationModel.swift b/VDS/Components/Carousel/CarouselPaginationModel.swift index 0abeeded..69766223 100644 --- a/VDS/Components/Carousel/CarouselPaginationModel.swift +++ b/VDS/Components/Carousel/CarouselPaginationModel.swift @@ -11,30 +11,16 @@ import UIKit /// Custom data type for pagination prop for 'Carousel' component. extension Carousel { public struct CarouselPaginationModel { + + /// Button icon property 'kind' is supported + public var kind: ButtonIcon.Kind - /// Previous button to show previous slide. - public var previousButton = ButtonIcon().with { - $0.kind = .lowContrast - $0.iconName = .leftCaret - $0.iconOffset = .init(x: -2, y: 0) - $0.icon.size = UIDevice.isIPad ? .small : .xsmall - $0.size = UIDevice.isIPad ? .large : .small - } - - /// Next button to show next slide. - public var nextButton = ButtonIcon().with { - $0.kind = .lowContrast - $0.iconName = .rightCaret - $0.iconOffset = .init(x: 2, y: 0) - $0.icon.size = UIDevice.isIPad ? .small : .xsmall - $0.size = UIDevice.isIPad ? .large : .small - } + /// Button icon property 'floating' is supported + public var floating: Bool public init(kind: ButtonIcon.Kind, floating: Bool) { - previousButton.kind = kind - nextButton.kind = kind - previousButton.floating = floating - nextButton.floating = floating + self.kind = kind + self.floating = floating } } } From 35e4cf0301133871a4dad6db21d1a72709ad2046 Mon Sep 17 00:00:00 2001 From: vasavk Date: Tue, 11 Jun 2024 15:16:52 +0530 Subject: [PATCH 05/31] Digital ACT-191 ONEAPP-7013 story: show slides on Scrollview --- VDS/Components/Carousel/Carousel.swift | 276 +++++++++++------- .../Carousel/CarouselPaginationModel.swift | 4 +- .../CarouselScrollbar/CarouselScrollbar.swift | 14 +- 3 files changed, 186 insertions(+), 108 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 4007f5fe..8c234351 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -30,6 +30,92 @@ open class Carousel: View { super.init(coder: coder) } + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- + /// Enum used to describe the aspect ratios used for this component. + public enum AspectRatio: String, CaseIterable { + case ratio1x1 = "1:1" + case ratio3x4 = "3:4" + case ratio4x3 = "4:3" + case ratio2x3 = "2:3" + case ratio3x2 = "3:2" + case ratio9x16 = "9:16" + case ratio16x9 = "16:9" + case ratio1x2 = "1:2" + case ratio2x1 = "2:1" + case none + } + + /// Enum used to describe the number of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. + public enum Layout: String, CaseIterable { + case oneUP = "1UP" + case twoUP = "2UP" + case threeUP = "3UP" + case fourUP = "4UP" + case fiveUP = "5UP" + case sixUP = "6UP" + + var value: Int { + switch self { + case .oneUP: + 1 + case .twoUP: + 2 + case .threeUP: + 3 + case .fourUP: + 4 + case .fiveUP: + 5 + case .sixUP: + 6 + } + } + } + + /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. + public enum Gutter: String, CaseIterable { + case twelvePX = "12px" + case twentyFourPX = "24px" + + var value: CGFloat { + switch self { + case .twelvePX: + VDSLayout.space3X + case .twentyFourPX: + VDSLayout.space6X + } + } + } + + /// Enum used to describe the pagination display for this component. + public enum PaginationDisplay: String, CaseIterable { + case onHover, persistent, none + } + + /// Enum used to describe the peek for this component. Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. + public enum Peek: String, CaseIterable { + case standard, minimum, none + } + + // TO DO: move to model class + /// Enum used to describe the vertical of slotAlignment. + public enum Vertical: String, CaseIterable { + case top, middle, bottom + } + + /// Enum used to describe the horizontal of slotAlignment. + public enum Horizontal: String, CaseIterable { + case left, center, right + } + + /// Enum used to describe the width of a fixed value or percentage of parent's width. + public enum Width { + case percentage(CGFloat) + case value(CGFloat) + } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -38,8 +124,30 @@ open class Carousel: View { /// Data used to render tilelets in the carousel. open var data: [Any] = [] { didSet { setNeedsUpdate() } } + + /// If provided, width of slots will be rendered based on this value. If omitted, default widths are rendered. + open var width : Width? { + get { _width } + set { + if let newValue { + switch newValue { + case .percentage(let percentage): + if percentage >= 10 && percentage <= 100.0 { + _width = newValue + } + case .value(let value): + if value > minimumSlotWidth { /*(size.minimumSlotWidth)*/ + _width = newValue + } + } + } else { + _width = nil + } + setNeedsUpdate() + } + } - /// Space between each tile. The default value will be 24px in tablet and 12px in mobile. + /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. open var gutter: Gutter { get { return _gutter } set { @@ -113,87 +221,7 @@ open class Carousel: View { /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } - - //-------------------------------------------------- - // MARK: - Enums - //-------------------------------------------------- - /// Enum used to describe the aspect ratios used for this component. - public enum AspectRatio: String, CaseIterable { - case ratio1x1 = "1:1" - case ratio3x4 = "3:4" - case ratio4x3 = "4:3" - case ratio2x3 = "2:3" - case ratio3x2 = "3:2" - case ratio9x16 = "9:16" - case ratio16x9 = "16:9" - case ratio1x2 = "1:2" - case ratio2x1 = "2:1" - case none - } - - /// Enum used to describe the number of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. - public enum Layout: String, CaseIterable { - case oneUP = "1UP" - case twoUP = "2UP" - case threeUP = "3UP" - case fourUP = "4UP" - case fiveUP = "5UP" - case sixUP = "6UP" - - var value: Int { - switch self { - case .oneUP: - 1 - case .twoUP: - 2 - case .threeUP: - 3 - case .fourUP: - 4 - case .fiveUP: - 5 - case .sixUP: - 6 - } - } - } - - /// Enum used to describe the number of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. - public enum Gutter: String, CaseIterable { - case twelvePX = "12px" - case twentyFourPX = "24px" - var value: CGFloat { - switch self { - case .twelvePX: - VDSLayout.space12X - case .twentyFourPX: - VDSLayout.space24X - } - } - } - - /// Enum used to describe the pagination display for this component. - public enum PaginationDisplay: String, CaseIterable { - case onHover, persistent, none - } - - /// Enum used to describe the peek for this component. Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. - public enum Peek: String, CaseIterable { - case standard, minimum, none - } - - // TO DO: move to model class - /// Enum used to describe the vertical of slotAlignment. - public enum Vertical: String, CaseIterable { - case top, middle, bottom - } - - /// Enum used to describe the horizontal of slotAlignment. - public enum Horizontal: String, CaseIterable { - case left, center, right - } - //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -204,7 +232,7 @@ open class Carousel: View { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill - $0.spacing = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X + $0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X $0.backgroundColor = .clear } @@ -254,19 +282,21 @@ open class Carousel: View { internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) internal var _paginationDisplay: PaginationDisplay = .none - internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space8X + internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX internal var _peek: Peek = .none internal var _numberOfSlides: Int = 1 + private var _width: Width? = nil private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } - private var containerStackHeightConstraint: NSLayoutConstraint? private var containerViewHeightConstraint: NSLayoutConstraint? // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. - let space = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X - + let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X + var slotHeight = 100.0 + var peekMinimum = 24.0 + var minimumSlotWidth = 0.0 //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -293,6 +323,7 @@ open class Carousel: View { // add scrollview scrollContainerView.addSubview(scrollView) + scrollView.pinToSuperView() // add pagination button icons scrollContainerView.addSubview(previousButton) @@ -308,37 +339,84 @@ open class Carousel: View { // add scroll container view & carousel scrollbar contentStackView.addArrangedSubview(scrollContainerView) contentStackView.addArrangedSubview(carouselScrollBar) - contentStackView.setCustomSpacing(space, after: scrollContainerView) + contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView) contentStackView .pinTop() .pinBottom() .pinLeading() .pinTrailing() - .heightGreaterThanEqualTo(space+containerSize.height) + .heightGreaterThanEqualTo(scrollbarTopSpace + containerSize.height) contentStackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } open override func updateView() { - containerViewHeightConstraint?.isActive = false super.updateView() - containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 200) - containerViewHeightConstraint?.isActive = true + containerViewHeightConstraint?.isActive = false + updatePaginationControls() + getSlotWidth() + // perform a loop to iterate each subView + scrollView.subviews.forEach { subView in + // removing subView from its parent view + subView.removeFromSuperview() + } + + // add carousel items + if data.count > 0 { + var xPos = gutter.value + for _ in 0...data.count - 1 { + let carouselSlot = View().with { + $0.clipsToBounds = true + $0.backgroundColor = .lightGray + } + scrollView.addSubview(carouselSlot) + carouselSlot + .pinTop() + .pinBottom() + .pinLeading(xPos) + .width(minimumSlotWidth) + .height(slotHeight) + xPos = xPos + minimumSlotWidth + gutter.value + } + scrollView.heightAnchor.constraint(equalToConstant: slotHeight).isActive = true + scrollView.contentSize = CGSize(width: xPos-minimumSlotWidth, height: slotHeight) + } + let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height + containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight) + containerViewHeightConstraint?.isActive = true + layoutIfNeeded() + } + + open override func reset() { + super.reset() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + func updatePaginationControls() { + containerView.surface = surface previousButton.isHidden = (paginationDisplay == .none) nextButton.isHidden = (paginationDisplay == .none) previousButton.kind = pagination.kind previousButton.floating = pagination.floating nextButton.kind = pagination.kind nextButton.floating = pagination.floating - layoutIfNeeded() + previousButton.surface = surface + nextButton.surface = surface } - open override func reset() { -// for subview in subviews { -// for recognizer in subview.gestureRecognizers ?? [] { -// subview.removeGestureRecognizer(recognizer) -// } -// } - super.reset() + func getSlotWidth() { + let actualWidth = containerView.frame.size.width + minimumSlotWidth = actualWidth - (CGFloat(layout.value) * gutter.value) + switch peek { + case .standard: + minimumSlotWidth = minimumSlotWidth - (3*peekMinimum) + case .minimum: + minimumSlotWidth = minimumSlotWidth - peekMinimum + case .none: + break + } + minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value) } } diff --git a/VDS/Components/Carousel/CarouselPaginationModel.swift b/VDS/Components/Carousel/CarouselPaginationModel.swift index 69766223..ab85d6a4 100644 --- a/VDS/Components/Carousel/CarouselPaginationModel.swift +++ b/VDS/Components/Carousel/CarouselPaginationModel.swift @@ -12,10 +12,10 @@ import UIKit extension Carousel { public struct CarouselPaginationModel { - /// Button icon property 'kind' is supported + /// Pagination supports Button icon property 'kind'. public var kind: ButtonIcon.Kind - /// Button icon property 'floating' is supported + /// Pagination supports Button icon property 'floating'. public var floating: Bool public init(kind: ButtonIcon.Kind, floating: Bool) { diff --git a/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift b/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift index 00e810d3..abba5017 100644 --- a/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift +++ b/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift @@ -45,13 +45,13 @@ open class CarouselScrollbar: View { } /// The number of slides that can appear at once in a set in a carousel container. - open var selectedLayout: Layout? { - get { return _selectedLayout } + open var layout: Layout? { + get { return _layout } set { if let newValue { - _selectedLayout = newValue + _layout = newValue } else { - _selectedLayout = .oneUP + _layout = .oneUP } setThumbWidth() scrollThumbToPosition(position) @@ -198,7 +198,7 @@ open class CarouselScrollbar: View { //-------------------------------------------------- // Sizes are from InVision design specs. internal var containerSize: CGSize { CGSize(width: 45, height: 44) } - internal var _selectedLayout: Layout = .oneUP + internal var _layout: Layout = .oneUP internal var _numberOfSlides: Int = 1 internal var totalPositions: Int = 1 internal var _position: Int = 1 @@ -329,7 +329,7 @@ open class CarouselScrollbar: View { // Compute track width and should maintain minimum thumb width if needed private func setThumbWidth() { - let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_selectedLayout.value) + let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_layout.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) @@ -362,7 +362,7 @@ open class CarouselScrollbar: View { } private func checkPositions() { - totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value))) + totalPositions = Int (ceil (Double(numberOfSlides) / Double(_layout.value))) } private func scrollThumbToPosition(_ position: Int) { From ba1ffdf39082184ac7ef395c15c943f40d1046e3 Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 13 Jun 2024 13:51:32 +0530 Subject: [PATCH 06/31] Digital ACT-191 ONEAPP-7013 story: added listeners to perform actions --- VDS/Components/Carousel/Carousel.swift | 126 ++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 12 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 8c234351..077fc546 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -91,10 +91,11 @@ open class Carousel: View { /// Enum used to describe the pagination display for this component. public enum PaginationDisplay: String, CaseIterable { - case onHover, persistent, none + case persistent, none } - /// Enum used to describe the peek for this component. Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. + /// Enum used to describe the peek for this component. + /// This is how much a tile is partially visible. It is measured by the distance between the edge of the tile and the edge of the viewport or carousel container. A peek can appear on the left and/or right edge of the carousel container or viewport, depending on the carousel’s scroll position. public enum Peek: String, CaseIterable { case standard, minimum, none } @@ -157,7 +158,7 @@ open class Carousel: View { } /// The amount of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. - open var layout: Layout { + open var layout: CarouselScrollbar.Layout { get { return _layout } set { _layout = newValue @@ -197,7 +198,7 @@ open class Carousel: View { } } - /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. + /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. /// The default value will be 12px in tablet and 8px in mobile. open var paginationInset: CGFloat { get { return _paginationInset } @@ -279,7 +280,7 @@ open class Carousel: View { open var onChangePublisher = PassthroughSubject() private var onChangeCancellable: AnyCancellable? - internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP + internal var _layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) internal var _paginationDisplay: PaginationDisplay = .none internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X @@ -294,6 +295,7 @@ open class Carousel: View { // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X + var slotHeight = 100.0 var peekMinimum = 24.0 var minimumSlotWidth = 0.0 @@ -351,7 +353,19 @@ open class Carousel: View { open override func updateView() { super.updateView() + + carouselScrollBar.numberOfSlides = data.count + carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position + carouselScrollBar.layout = _layout + carouselScrollBar.isHidden = (Int((Float(carouselScrollBar.numberOfSlides)) * Float(carouselScrollBar._layout.value)) <= 1) ? true : false + + // When peek is set to ‘none,’ pagination controls are automatically set to persistent. + if peek == .none { + paginationDisplay = .persistent + } + containerViewHeightConstraint?.isActive = false + containerStackHeightConstraint?.isActive = false updatePaginationControls() getSlotWidth() @@ -363,11 +377,11 @@ open class Carousel: View { // add carousel items if data.count > 0 { - var xPos = gutter.value - for _ in 0...data.count - 1 { + var xPos = 0.0 + for x in 0...data.count - 1 { let carouselSlot = View().with { $0.clipsToBounds = true - $0.backgroundColor = .lightGray + $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) } scrollView.addSubview(carouselSlot) carouselSlot @@ -379,25 +393,74 @@ open class Carousel: View { xPos = xPos + minimumSlotWidth + gutter.value } scrollView.heightAnchor.constraint(equalToConstant: slotHeight).isActive = true - scrollView.contentSize = CGSize(width: xPos-minimumSlotWidth, height: slotHeight) + scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) } + let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height - containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight) + if carouselScrollBar.isHidden { + containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight) + containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: slotHeight) + } else { + containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: containerHeight) + containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight) + } containerViewHeightConstraint?.isActive = true + containerStackHeightConstraint?.isActive = true + + addlisteners() layoutIfNeeded() } open override func reset() { + for subview in subviews { + for recognizer in subview.gestureRecognizers ?? [] { + subview.removeGestureRecognizer(recognizer) + } + } super.reset() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- + func addlisteners() { + nextButton.onClick = { _ in self.nextButtonClick() } + previousButton.onClick = { _ in self.previousButtonClick() } + + //setup test page to show scrubber id was changed +// carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in +// guard let self else { return } +// updateScrollPosition(position: scrubberId, callbackText:"onScrubberDrag") +// } + + /// will be called when the thumb move forward. + carouselScrollBar.onMoveForward = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onMoveForward") + } + + /// will be called when the thumb move backward. + carouselScrollBar.onMoveBackward = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward") + } + + /// will be called when the thumb touch start. + carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart") + } + + /// will be called when the thumb touch end. + carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd") + } + } + func updatePaginationControls() { containerView.surface = surface - previousButton.isHidden = (paginationDisplay == .none) - nextButton.isHidden = (paginationDisplay == .none) + showPaginationControls() previousButton.kind = pagination.kind previousButton.floating = pagination.floating nextButton.kind = pagination.kind @@ -419,4 +482,43 @@ open class Carousel: View { } minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value) } + + func nextButtonClick() { + carouselScrollBar.position = carouselScrollBar.position+1 + showPaginationControls() + updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") + } + + func previousButtonClick() { + carouselScrollBar.position = carouselScrollBar.position-1 + showPaginationControls() + updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") + } + + func updateScrollPosition(position: Int, callbackText: String) { + if carouselScrollBar.numberOfSlides > 0 { + let contentOffsetWidth = scrollView.contentSize.width + let totalPositions = totalPositions() + let multiplier: Float = (position == 1) ? 0 : Float((position)-1) / Float(totalPositions) + let xPos = (position == totalPositions) ? (contentOffsetWidth - containerView.frame.size.width) : CGFloat(Float(contentOffsetWidth) * multiplier) + carouselScrollBar.scrubberId = position + let yPos = scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) + showPaginationControls() + } + } + + private func totalPositions() -> Int { + return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value))) + } + + func showPaginationControls() { + if carouselScrollBar.numberOfSlides == _layout.value { + previousButton.isHidden = true + nextButton.isHidden = true + } else { + previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none) + nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none) + } + } } From de8a2ebdeeed4ca1ce8cbb3b4841505cb993e7f5 Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 13 Jun 2024 14:13:27 +0530 Subject: [PATCH 07/31] Digital ACT-191 ONEAPP-7013 story: Using custom container size and custom icon sizes for positioning controls --- VDS/Components/Carousel/Carousel.swift | 41 +++++--------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 077fc546..95abb990 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -47,33 +47,6 @@ open class Carousel: View { case none } - /// Enum used to describe the number of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. - public enum Layout: String, CaseIterable { - case oneUP = "1UP" - case twoUP = "2UP" - case threeUP = "3UP" - case fourUP = "4UP" - case fiveUP = "5UP" - case sixUP = "6UP" - - var value: Int { - switch self { - case .oneUP: - 1 - case .twoUP: - 2 - case .threeUP: - 3 - case .fourUP: - 4 - case .fiveUP: - 5 - case .sixUP: - 6 - } - } - } - /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. public enum Gutter: String, CaseIterable { case twelvePX = "12px" @@ -263,8 +236,8 @@ open class Carousel: View { $0.kind = .lowContrast $0.iconName = .leftCaret $0.iconOffset = .init(x: -2, y: 0) - $0.icon.size = UIDevice.isIPad ? .small : .xsmall - $0.size = UIDevice.isIPad ? .large : .small + $0.customContainerSize = UIDevice.isIPad ? 40 : 28 + $0.icon.customSize = UIDevice.isIPad ? 16 : 12 } /// Next button to show next slide. @@ -272,8 +245,8 @@ open class Carousel: View { $0.kind = .lowContrast $0.iconName = .rightCaret $0.iconOffset = .init(x: 2, y: 0) - $0.icon.size = UIDevice.isIPad ? .small : .xsmall - $0.size = UIDevice.isIPad ? .large : .small + $0.customContainerSize = UIDevice.isIPad ? 40 : 28 + $0.icon.customSize = UIDevice.isIPad ? 16 : 12 } /// A publisher for when the scrubber position changes. Passes parameters (position). @@ -355,10 +328,10 @@ open class Carousel: View { super.updateView() carouselScrollBar.numberOfSlides = data.count - carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position carouselScrollBar.layout = _layout - carouselScrollBar.isHidden = (Int((Float(carouselScrollBar.numberOfSlides)) * Float(carouselScrollBar._layout.value)) <= 1) ? true : false - + carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position + carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false + // When peek is set to ‘none,’ pagination controls are automatically set to persistent. if peek == .none { paginationDisplay = .persistent From a829df86e58ae23c67826aa00ce83be6ebd077b4 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 13:17:29 +0530 Subject: [PATCH 08/31] Digital ACT-191 ONEAPP-7013 story: Fixed Constraints issue and refactored Code --- VDS/Components/Carousel/Carousel.swift | 113 +++++++++++-------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 95abb990..0486af1d 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -33,25 +33,11 @@ open class Carousel: View { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- - /// Enum used to describe the aspect ratios used for this component. - public enum AspectRatio: String, CaseIterable { - case ratio1x1 = "1:1" - case ratio3x4 = "3:4" - case ratio4x3 = "4:3" - case ratio2x3 = "2:3" - case ratio3x2 = "3:2" - case ratio9x16 = "9:16" - case ratio16x9 = "16:9" - case ratio1x2 = "1:2" - case ratio2x1 = "2:1" - case none - } - /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. public enum Gutter: String, CaseIterable { case twelvePX = "12px" case twentyFourPX = "24px" - + var value: CGFloat { switch self { case .twelvePX: @@ -61,7 +47,7 @@ open class Carousel: View { } } } - + /// Enum used to describe the pagination display for this component. public enum PaginationDisplay: String, CaseIterable { case persistent, none @@ -72,8 +58,7 @@ open class Carousel: View { public enum Peek: String, CaseIterable { case standard, minimum, none } - - // TO DO: move to model class + /// Enum used to describe the vertical of slotAlignment. public enum Vertical: String, CaseIterable { case top, middle, bottom @@ -94,11 +79,11 @@ open class Carousel: View { // MARK: - Public Properties //-------------------------------------------------- /// Aspect-ratio options for tilelet in the carousel. If 'none' is passed, the tilelet will take the height of the tallest item in the carousel. - open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } - + open var aspectRatio: Tilelet.AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } + /// Data used to render tilelets in the carousel. open var data: [Any] = [] { didSet { setNeedsUpdate() } } - + /// If provided, width of slots will be rendered based on this value. If omitted, default widths are rendered. open var width : Width? { get { _width } @@ -129,7 +114,7 @@ open class Carousel: View { setNeedsUpdate() } } - + /// The amount of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. open var layout: CarouselScrollbar.Layout { get { return _layout } @@ -138,9 +123,9 @@ open class Carousel: View { setNeedsUpdate() } } - + /// A callback when moving the carousel. Returns event object and selectedGroupIndex. - open var onChange: ((Int) -> Void)? { // TO DO: return object and index + open var onChange: ((Int) -> Void)? { get { nil } set { onChangeCancellable?.cancel() @@ -161,7 +146,7 @@ open class Carousel: View { setNeedsUpdate() } } - + /// If provided, will determine the conditions to render the pagination arrows. open var paginationDisplay: PaginationDisplay { get { return _paginationDisplay } @@ -170,7 +155,7 @@ open class Carousel: View { setNeedsUpdate() } } - + /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. /// The default value will be 12px in tablet and 8px in mobile. open var paginationInset: CGFloat { @@ -189,19 +174,19 @@ open class Carousel: View { setNeedsUpdate() } } - + /// The initial visible slide's index in the carousel. open var selectedIndex: Int? { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- // Sizes are from InVision design specs. internal var containerSize: CGSize { CGSize(width: 320, height: 44) } - + private let contentStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical @@ -231,28 +216,32 @@ open class Carousel: View { $0.backgroundColor = .clear } - /// Previous button to show previous slide. - private var previousButton = ButtonIcon().with { - $0.kind = .lowContrast - $0.iconName = .leftCaret - $0.iconOffset = .init(x: -2, y: 0) - $0.customContainerSize = UIDevice.isIPad ? 40 : 28 - $0.icon.customSize = UIDevice.isIPad ? 16 : 12 - } - - /// Next button to show next slide. - private var nextButton = ButtonIcon().with { - $0.kind = .lowContrast - $0.iconName = .rightCaret - $0.iconOffset = .init(x: 2, y: 0) - $0.customContainerSize = UIDevice.isIPad ? 40 : 28 - $0.icon.customSize = UIDevice.isIPad ? 16 : 12 - } + /// Previous button to show previous slide. + private var previousButton = ButtonIcon().with { + $0.kind = .lowContrast + $0.iconName = .leftCaret + $0.iconOffset = .init(x: -2, y: 0) + $0.customContainerSize = UIDevice.isIPad ? 40 : 28 + $0.icon.customSize = UIDevice.isIPad ? 16 : 12 + } + + /// Next button to show next slide. + private var nextButton = ButtonIcon().with { + $0.kind = .lowContrast + $0.iconName = .rightCaret + $0.iconOffset = .init(x: 2, y: 0) + $0.customContainerSize = UIDevice.isIPad ? 40 : 28 + $0.icon.customSize = UIDevice.isIPad ? 16 : 12 + } /// A publisher for when the scrubber position changes. Passes parameters (position). open var onChangePublisher = PassthroughSubject() private var onChangeCancellable: AnyCancellable? - + + /// A publisher for when the carousel moves. Passes parameters (data). + open var onScrollPublisher = PassthroughSubject, Never>() + private var onScrollCancellable: AnyCancellable? + internal var _layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) internal var _paginationDisplay: PaginationDisplay = .none @@ -260,12 +249,12 @@ open class Carousel: View { internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX internal var _peek: Peek = .none internal var _numberOfSlides: Int = 1 - + private var _width: Width? = nil private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } private var containerStackHeightConstraint: NSLayoutConstraint? private var containerViewHeightConstraint: NSLayoutConstraint? - + // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X @@ -282,7 +271,7 @@ open class Carousel: View { open override func setup() { super.setup() isAccessibilityElement = false - + // add containerView addSubview(containerView) containerView @@ -305,12 +294,12 @@ open class Carousel: View { previousButton .pinLeading(paginationInset) .pinCenterY() - + scrollContainerView.addSubview(nextButton) nextButton .pinTrailing(paginationInset) .pinCenterY() - + // add scroll container view & carousel scrollbar contentStackView.addArrangedSubview(scrollContainerView) contentStackView.addArrangedSubview(carouselScrollBar) @@ -320,13 +309,15 @@ open class Carousel: View { .pinBottom() .pinLeading() .pinTrailing() - .heightGreaterThanEqualTo(scrollbarTopSpace + containerSize.height) + .heightGreaterThanEqualTo(containerSize.height) contentStackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + addlisteners() } open override func updateView() { super.updateView() - + carouselScrollBar.numberOfSlides = data.count carouselScrollBar.layout = _layout carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position @@ -354,7 +345,7 @@ open class Carousel: View { for x in 0...data.count - 1 { let carouselSlot = View().with { $0.clipsToBounds = true - $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) + $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) } scrollView.addSubview(carouselSlot) carouselSlot @@ -365,7 +356,6 @@ open class Carousel: View { .height(slotHeight) xPos = xPos + minimumSlotWidth + gutter.value } - scrollView.heightAnchor.constraint(equalToConstant: slotHeight).isActive = true scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) } @@ -380,7 +370,6 @@ open class Carousel: View { containerViewHeightConstraint?.isActive = true containerStackHeightConstraint?.isActive = true - addlisteners() layoutIfNeeded() } @@ -401,10 +390,10 @@ open class Carousel: View { previousButton.onClick = { _ in self.previousButtonClick() } //setup test page to show scrubber id was changed -// carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in -// guard let self else { return } -// updateScrollPosition(position: scrubberId, callbackText:"onScrubberDrag") -// } + // carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in + // guard let self else { return } + // updateScrollPosition(position: scrubberId, callbackText:"onScrubberDrag") + // } /// will be called when the thumb move forward. carouselScrollBar.onMoveForward = { [weak self] scrubberId in @@ -467,7 +456,7 @@ open class Carousel: View { showPaginationControls() updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") } - + func updateScrollPosition(position: Int, callbackText: String) { if carouselScrollBar.numberOfSlides > 0 { let contentOffsetWidth = scrollView.contentSize.width From 0a27c27189927b3731c52a85434889e983c67016 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 18:02:57 +0530 Subject: [PATCH 09/31] Digital ACT-191 ONEAPP-7013 story: add corner radius to slot, supporting to dynamic pagination inset --- VDS/Components/Carousel/Carousel.swift | 38 +++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 0486af1d..cf52506b 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -161,8 +161,9 @@ open class Carousel: View { open var paginationInset: CGFloat { get { return _paginationInset } set { - _paginationInset = newValue - setNeedsUpdate() + let minValue = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X + _paginationInset = (newValue < minValue) ? minValue : newValue + updatePaginationInset() } } @@ -180,7 +181,7 @@ open class Carousel: View { /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -254,7 +255,9 @@ open class Carousel: View { private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } private var containerStackHeightConstraint: NSLayoutConstraint? private var containerViewHeightConstraint: NSLayoutConstraint? - + private var prevButtonLeadingConstraint: NSLayoutConstraint? + private var nextButtonTrailingConstraint: NSLayoutConstraint? + // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X @@ -292,12 +295,12 @@ open class Carousel: View { // add pagination button icons scrollContainerView.addSubview(previousButton) previousButton - .pinLeading(paginationInset) + .pinLeadingGreaterThanOrEqualTo() .pinCenterY() scrollContainerView.addSubview(nextButton) nextButton - .pinTrailing(paginationInset) + .pinTrailingLessThanOrEqualTo() .pinCenterY() // add scroll container view & carousel scrollbar @@ -314,10 +317,9 @@ open class Carousel: View { addlisteners() } - + open override func updateView() { super.updateView() - carouselScrollBar.numberOfSlides = data.count carouselScrollBar.layout = _layout carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position @@ -327,7 +329,7 @@ open class Carousel: View { if peek == .none { paginationDisplay = .persistent } - + containerViewHeightConstraint?.isActive = false containerStackHeightConstraint?.isActive = false updatePaginationControls() @@ -354,6 +356,7 @@ open class Carousel: View { .pinLeading(xPos) .width(minimumSlotWidth) .height(slotHeight) + carouselSlot.layer.cornerRadius = 12.0 xPos = xPos + minimumSlotWidth + gutter.value } scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) @@ -388,13 +391,7 @@ open class Carousel: View { func addlisteners() { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } - - //setup test page to show scrubber id was changed - // carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in - // guard let self else { return } - // updateScrollPosition(position: scrubberId, callbackText:"onScrubberDrag") - // } - + /// will be called when the thumb move forward. carouselScrollBar.onMoveForward = { [weak self] scrubberId in guard let self else { return } @@ -431,6 +428,15 @@ open class Carousel: View { nextButton.surface = surface } + func updatePaginationInset() { + prevButtonLeadingConstraint?.isActive = false + nextButtonTrailingConstraint?.isActive = false + prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset) + nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset) + prevButtonLeadingConstraint?.isActive = true + nextButtonTrailingConstraint?.isActive = true + } + func getSlotWidth() { let actualWidth = containerView.frame.size.width minimumSlotWidth = actualWidth - (CGFloat(layout.value) * gutter.value) From 993afcc367ff8da31643eac5c2b6215295101c4e Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 20:32:22 +0530 Subject: [PATCH 10/31] updated to VDSCoreTokens --- VDS/Components/Carousel/Carousel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index cf52506b..4ad8ef8c 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -7,7 +7,7 @@ import Foundation import UIKit -import VDSTokens +import VDSCoreTokens import Combine /// A carousel is a collection of related content in a row that a customer can navigate through horizontally. From c00ddc68fd2d37c92641294241879a9df7746faf Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Mon, 24 Jun 2024 16:31:00 +0530 Subject: [PATCH 11/31] Digital ACT-191 ONEAPP-7013 story: changes to pagination inset --- VDS/Components/Carousel/Carousel.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 4ad8ef8c..6c091ce2 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -157,12 +157,11 @@ open class Carousel: View { } /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. - /// The default value will be 12px in tablet and 8px in mobile. + /// The default value will be 12px in tablet and 8px in mobile. These values are the default in order to avoid overlapping content within the carousel. open var paginationInset: CGFloat { get { return _paginationInset } set { - let minValue = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X - _paginationInset = (newValue < minValue) ? minValue : newValue + _paginationInset = newValue updatePaginationInset() } } From 0b47d70352912b61b433ae44fe93841f63a8c210 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Mon, 24 Jun 2024 17:04:36 +0530 Subject: [PATCH 12/31] Digital ACT-191 ONEAPP-7013 story: changes to the default view of the carousel with one peek visible --- VDS/Components/Carousel/Carousel.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 6c091ce2..378289c7 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -441,7 +441,13 @@ open class Carousel: View { minimumSlotWidth = actualWidth - (CGFloat(layout.value) * gutter.value) switch peek { case .standard: - minimumSlotWidth = minimumSlotWidth - (3*peekMinimum) + // Supported for all Tablet viewports and layouts. + // Supported only for 1up layouts on Mobile viewports. + if UIDevice.isIPad { + minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 2)) + } else if layout == .oneUP { + minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4) + } case .minimum: minimumSlotWidth = minimumSlotWidth - peekMinimum case .none: From 0840ed5ed31819f27012c79fdd0ef9be7c210aba Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Mon, 24 Jun 2024 18:42:07 +0530 Subject: [PATCH 13/31] Digital ACT-191 ONEAPP-7013 story: Adjust slot width if scrollbar suppressed and/or based on peek value --- VDS/Components/Carousel/Carousel.swift | 42 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 378289c7..80c55b16 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -247,7 +247,7 @@ open class Carousel: View { internal var _paginationDisplay: PaginationDisplay = .none internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX - internal var _peek: Peek = .none + internal var _peek: Peek = .standard internal var _numberOfSlides: Int = 1 private var _width: Width? = nil @@ -324,11 +324,17 @@ open class Carousel: View { carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false - // When peek is set to ‘none,’ pagination controls are automatically set to persistent. + // Mobile/Tablet layouts without peek - must show pagination controls. + // If peek is ‘none’, pagination controls should show. So set to persistent. if peek == .none { paginationDisplay = .persistent } + // Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard. + if UIDevice.isIPad && peek == .minimum { + peek = .standard + } + containerViewHeightConstraint?.isActive = false containerStackHeightConstraint?.isActive = false updatePaginationControls() @@ -438,20 +444,26 @@ open class Carousel: View { func getSlotWidth() { let actualWidth = containerView.frame.size.width - minimumSlotWidth = actualWidth - (CGFloat(layout.value) * gutter.value) - switch peek { - case .standard: - // Supported for all Tablet viewports and layouts. - // Supported only for 1up layouts on Mobile viewports. - if UIDevice.isIPad { - minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 2)) - } else if layout == .oneUP { - minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4) + let isScrollbarSuppressed = data.count > 0 && layout.value == data.count + let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum + let isPeekNone: Bool = peek == .none + minimumSlotWidth = isScrollbarSuppressed || isPeekMinimumOnTablet || isPeekNone ? actualWidth - ((CGFloat(layout.value)-1) * gutter.value): actualWidth - (CGFloat(layout.value) * gutter.value) + if !isScrollbarSuppressed { + switch peek { + case .standard: + // Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports. + if UIDevice.isIPad { + minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 3)) + } else if layout == .oneUP { + minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4) + } + case .minimum: + // Peek Mimumum Width: 24px from edge of container (at the default view of the carousel with one peek visible) + // Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard. + minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum + case .none: + break } - case .minimum: - minimumSlotWidth = minimumSlotWidth - peekMinimum - case .none: - break } minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value) } From c51093cfaf3788e567c4ee29c560e5b104a9125b Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Fri, 28 Jun 2024 17:04:00 +0530 Subject: [PATCH 14/31] Digital ACT-191 ONEAPP-7013 story: update view as per aspect ratio --- VDS/Components/Carousel/Carousel.swift | 46 +++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 80c55b16..c5d0ab0e 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -355,6 +355,8 @@ open class Carousel: View { $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) } scrollView.addSubview(carouselSlot) + let size = ratioSize(for: minimumSlotWidth) + slotHeight = size.height carouselSlot .pinTop() .pinBottom() @@ -382,17 +384,51 @@ open class Carousel: View { } open override func reset() { - for subview in subviews { - for recognizer in subview.gestureRecognizers ?? [] { - subview.removeGestureRecognizer(recognizer) - } - } super.reset() + shouldUpdateView = false + aspectRatio = .ratio1x1 + layout = UIDevice.isIPad ? .threeUP : .oneUP + pagination = .init(kind: .lowContrast, floating: true) + paginationDisplay = .none + paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X + gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX + peek = .standard + width = nil } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- + private func ratioSize(for width: CGFloat) -> CGSize { + var height: CGFloat = width + + switch aspectRatio { + case .ratio1x1: + break; + case .ratio3x4: + height = (4 / 3) * width + case .ratio4x3: + height = (3 / 4) * width + case .ratio2x3: + height = (3 / 2) * width + case .ratio3x2: + height = (2 / 3) * width + case .ratio9x16: + height = (16 / 9) * width + case .ratio16x9: + height = (9 / 16) * width + case .ratio1x2: + height = (2 / 1) * width + case .ratio2x1: + height = (1 / 2) * width + + default: + break + } + + return CGSize(width: width, height: height) + } + func addlisteners() { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } From fa5051d258f21cc82f4a0da0c2690280819a3a7f Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Mon, 1 Jul 2024 10:32:06 +0530 Subject: [PATCH 15/31] Digital ACT-191 ONEAPP-7013 story: flexible width support, Carousel can be set to any pixel or percentage --- VDS/Components/Carousel/Carousel.swift | 124 +++++++++++++++---------- 1 file changed, 74 insertions(+), 50 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index c5d0ab0e..5b5c99bf 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -79,7 +79,7 @@ open class Carousel: View { // MARK: - Public Properties //-------------------------------------------------- /// Aspect-ratio options for tilelet in the carousel. If 'none' is passed, the tilelet will take the height of the tallest item in the carousel. - open var aspectRatio: Tilelet.AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } + open var aspectRatio: Tilelet.AspectRatio = .none { didSet { setNeedsUpdate() } } /// Data used to render tilelets in the carousel. open var data: [Any] = [] { didSet { setNeedsUpdate() } } @@ -92,10 +92,13 @@ open class Carousel: View { switch newValue { case .percentage(let percentage): if percentage >= 10 && percentage <= 100.0 { - _width = newValue + let expectedWidth = safeAreaLayoutGuide.layoutFrame.size.width * (percentage/100) + if expectedWidth > carouselScrollbarMinWidth { + _width = newValue + } } case .value(let value): - if value > minimumSlotWidth { /*(size.minimumSlotWidth)*/ + if value > carouselScrollbarMinWidth { _width = newValue } } @@ -256,6 +259,7 @@ open class Carousel: View { private var containerViewHeightConstraint: NSLayoutConstraint? private var prevButtonLeadingConstraint: NSLayoutConstraint? private var nextButtonTrailingConstraint: NSLayoutConstraint? + private var containerLeadingConstraint: NSLayoutConstraint? // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X @@ -263,6 +267,7 @@ open class Carousel: View { var slotHeight = 100.0 var peekMinimum = 24.0 var minimumSlotWidth = 0.0 + var carouselScrollbarMinWidth = 96.0 //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -279,11 +284,12 @@ open class Carousel: View { containerView .pinTop() .pinBottom() - .pinLeading() + .pinLeadingGreaterThanOrEqualTo() .pinTrailing() .heightGreaterThanEqualTo(containerSize.height) - containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() - + containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate() + containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 0) + // add content stackview containerView.addSubview(contentStackView) @@ -312,13 +318,30 @@ open class Carousel: View { .pinLeading() .pinTrailing() .heightGreaterThanEqualTo(containerSize.height) - contentStackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() addlisteners() } open override func updateView() { super.updateView() + + if containerView.frame.size.width > 0 { + if let width { + containerLeadingConstraint?.deactivate() + switch width { + case .value(let value): + var expectedWidth = value + let fullWidth = safeAreaLayoutGuide.layoutFrame.size.width + expectedWidth = expectedWidth > fullWidth ? fullWidth : expectedWidth + containerLeadingConstraint?.constant = safeAreaLayoutGuide.layoutFrame.size.width - expectedWidth + case .percentage(let percentage): + let expectedWidth = safeAreaLayoutGuide.layoutFrame.size.width * (percentage/100) + containerLeadingConstraint?.constant = safeAreaLayoutGuide.layoutFrame.size.width - expectedWidth + } + containerLeadingConstraint?.activate() + } + } + carouselScrollBar.numberOfSlides = data.count carouselScrollBar.layout = _layout carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position @@ -335,58 +358,59 @@ open class Carousel: View { peek = .standard } - containerViewHeightConstraint?.isActive = false - containerStackHeightConstraint?.isActive = false updatePaginationControls() getSlotWidth() - - // perform a loop to iterate each subView - scrollView.subviews.forEach { subView in - // removing subView from its parent view - subView.removeFromSuperview() - } - - // add carousel items - if data.count > 0 { - var xPos = 0.0 - for x in 0...data.count - 1 { - let carouselSlot = View().with { - $0.clipsToBounds = true - $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) - } - scrollView.addSubview(carouselSlot) - let size = ratioSize(for: minimumSlotWidth) - slotHeight = size.height - carouselSlot - .pinTop() - .pinBottom() - .pinLeading(xPos) - .width(minimumSlotWidth) - .height(slotHeight) - carouselSlot.layer.cornerRadius = 12.0 - xPos = xPos + minimumSlotWidth + gutter.value + + if containerView.frame.size.width > 0 { + containerViewHeightConstraint?.isActive = false + containerStackHeightConstraint?.isActive = false + + // perform a loop to iterate each subView + scrollView.subviews.forEach { subView in + // removing subView from its parent view + subView.removeFromSuperview() } - scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) + + // add carousel items + if data.count > 0 { + var xPos = 0.0 + for x in 0...data.count - 1 { + let carouselSlot = View().with { + $0.clipsToBounds = true + $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) + } + scrollView.addSubview(carouselSlot) + let size = ratioSize(for: minimumSlotWidth) + slotHeight = size.height + carouselSlot + .pinTop() + .pinBottom() + .pinLeading(xPos) + .width(minimumSlotWidth) + .height(slotHeight) + carouselSlot.layer.cornerRadius = 12.0 + xPos = xPos + minimumSlotWidth + gutter.value + } + scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) + } + + let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height + if carouselScrollBar.isHidden { + containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight) + containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: slotHeight) + } else { + containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: containerHeight) + containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight) + } + containerViewHeightConstraint?.isActive = true + containerStackHeightConstraint?.isActive = true } - - let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height - if carouselScrollBar.isHidden { - containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight) - containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: slotHeight) - } else { - containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: containerHeight) - containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight) - } - containerViewHeightConstraint?.isActive = true - containerStackHeightConstraint?.isActive = true - - layoutIfNeeded() } open override func reset() { super.reset() shouldUpdateView = false - aspectRatio = .ratio1x1 + aspectRatio = .none layout = UIDevice.isIPad ? .threeUP : .oneUP pagination = .init(kind: .lowContrast, floating: true) paginationDisplay = .none From 6f5e94887e6de1f480ff95e7abe442516e177ac0 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Mon, 1 Jul 2024 14:13:44 +0530 Subject: [PATCH 16/31] activating leading constratint --- VDS/Components/Carousel/Carousel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 5b5c99bf..e461bc99 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -289,6 +289,7 @@ open class Carousel: View { .heightGreaterThanEqualTo(containerSize.height) containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate() containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 0) + containerLeadingConstraint?.activate() // add content stackview containerView.addSubview(contentStackView) From f13451ea37fb80aea10823fc4b86eb1ce42b2303 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Mon, 1 Jul 2024 15:03:00 +0530 Subject: [PATCH 17/31] Digital ACT-191 ONEAPP-7013 story: Standard Peek supported for all Tablet viewports and layouts and for 1up layouts on Mobile viewports only. --- VDS/Components/Carousel/Carousel.swift | 46 +++++++++++++++----------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index e461bc99..0347efc0 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -358,10 +358,34 @@ open class Carousel: View { if UIDevice.isIPad && peek == .minimum { peek = .standard } + + // Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports. + if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP { + peek = .minimum + } updatePaginationControls() getSlotWidth() - + addCarouselSlots() + } + + open override func reset() { + super.reset() + shouldUpdateView = false + aspectRatio = .none + layout = UIDevice.isIPad ? .threeUP : .oneUP + pagination = .init(kind: .lowContrast, floating: true) + paginationDisplay = .none + paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X + gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX + peek = .standard + width = nil + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func addCarouselSlots() { if containerView.frame.size.width > 0 { containerViewHeightConstraint?.isActive = false containerStackHeightConstraint?.isActive = false @@ -408,22 +432,6 @@ open class Carousel: View { } } - open override func reset() { - super.reset() - shouldUpdateView = false - aspectRatio = .none - layout = UIDevice.isIPad ? .threeUP : .oneUP - pagination = .init(kind: .lowContrast, floating: true) - paginationDisplay = .none - paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X - gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX - peek = .standard - width = nil - } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- private func ratioSize(for width: CGFloat) -> CGSize { var height: CGFloat = width @@ -543,10 +551,10 @@ open class Carousel: View { func updateScrollPosition(position: Int, callbackText: String) { if carouselScrollBar.numberOfSlides > 0 { - let contentOffsetWidth = scrollView.contentSize.width + let scrollContentSizeWidth = scrollView.contentSize.width let totalPositions = totalPositions() let multiplier: Float = (position == 1) ? 0 : Float((position)-1) / Float(totalPositions) - let xPos = (position == totalPositions) ? (contentOffsetWidth - containerView.frame.size.width) : CGFloat(Float(contentOffsetWidth) * multiplier) + let xPos = (position == totalPositions) ? (scrollContentSizeWidth - containerView.frame.size.width) : CGFloat(Float(scrollContentSizeWidth) * multiplier) carouselScrollBar.scrubberId = position let yPos = scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) From 668b20f77b8942f76a29f3a0ca06b02fc4fe00f7 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Tue, 2 Jul 2024 13:22:39 +0530 Subject: [PATCH 18/31] Digital ACT-191 ONEAPP-7013 story: Updating scrollbar position on scrolling carousel content --- VDS/Components/Carousel/Carousel.swift | 57 ++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 0347efc0..498e1dc5 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -365,7 +365,6 @@ open class Carousel: View { } updatePaginationControls() - getSlotWidth() addCarouselSlots() } @@ -386,6 +385,7 @@ open class Carousel: View { // MARK: - Private Methods //-------------------------------------------------- private func addCarouselSlots() { + getSlotWidth() if containerView.frame.size.width > 0 { containerViewHeightConstraint?.isActive = false containerStackHeightConstraint?.isActive = false @@ -405,6 +405,7 @@ open class Carousel: View { $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) } scrollView.addSubview(carouselSlot) + scrollView.delegate = self let size = ratioSize(for: minimumSlotWidth) slotHeight = size.height carouselSlot @@ -553,15 +554,49 @@ open class Carousel: View { if carouselScrollBar.numberOfSlides > 0 { let scrollContentSizeWidth = scrollView.contentSize.width let totalPositions = totalPositions() - let multiplier: Float = (position == 1) ? 0 : Float((position)-1) / Float(totalPositions) - let xPos = (position == totalPositions) ? (scrollContentSizeWidth - containerView.frame.size.width) : CGFloat(Float(scrollContentSizeWidth) * multiplier) - carouselScrollBar.scrubberId = position + var xPos = 0.0 + if position == 1 { + xPos = 0.0 + } else if position == totalPositions { + xPos = scrollContentSizeWidth - containerView.frame.size.width + } else { + let isScrollbarSuppressed = data.count > 0 && layout.value == data.count + let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum + if !isScrollbarSuppressed { + let subpart = minimumSlotWidth + gutter.value + let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(subpart)) + switch peek { + case .standard: + if UIDevice.isIPad { + xPos = xPosition - (minimumSlotWidth/(CGFloat(layout.value) + 3))/2 + } else if layout == .oneUP { + xPos = xPosition - gutter.value - (minimumSlotWidth/4)/2 + } + case .minimum: + xPos = isPeekMinimumOnTablet ? xPosition : xPosition - peekMinimum/2 + case .none: + xPos = xPosition + } + } + } + carouselScrollBar.scrubberId = position+1 let yPos = scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) showPaginationControls() } } + func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) { + let scrollContentSizeWidth = scrollView.contentSize.width + let totalPositions = totalPositions() + let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions)))) + let remindSpace = Int(targetContentOffsetXPos) % layoutSpace + var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1 + contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos + carouselScrollBar.position = contentPos + updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved") + } + private func totalPositions() -> Int { return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value))) } @@ -576,3 +611,17 @@ open class Carousel: View { } } } + +extension Carousel: UIScrollViewDelegate { + //-------------------------------------------------- + // MARK: - UIScrollView Delegate + //-------------------------------------------------- + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x) + } + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + var visibleRect = CGRect() + visibleRect.origin = scrollView.contentOffset + updateScrollbarPosition(targetContentOffsetXPos: visibleRect.origin.x) + } +} From 8619c641096645cf2b80bf8a288cecb93030074b Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Tue, 2 Jul 2024 14:47:20 +0530 Subject: [PATCH 19/31] Digital ACT-191 ONEAPP-7013 story: refactored code --- VDS/Components/Carousel/Carousel.swift | 247 +++++++++++++------------ 1 file changed, 126 insertions(+), 121 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 498e1dc5..dbc10d98 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -183,13 +183,11 @@ open class Carousel: View { /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - // Sizes are from InVision design specs. - internal var containerSize: CGSize { CGSize(width: 320, height: 44) } - + internal var containerSize: CGSize { CGSize(width: frame.size.width, height: 44) } private let contentStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical @@ -260,7 +258,7 @@ open class Carousel: View { private var prevButtonLeadingConstraint: NSLayoutConstraint? private var nextButtonTrailingConstraint: NSLayoutConstraint? private var containerLeadingConstraint: NSLayoutConstraint? - + // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X @@ -268,18 +266,21 @@ open class Carousel: View { var peekMinimum = 24.0 var minimumSlotWidth = 0.0 var carouselScrollbarMinWidth = 96.0 + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- + /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() } + /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() isAccessibilityElement = false - // add containerView + // Add containerView addSubview(containerView) containerView .pinTop() @@ -287,18 +288,19 @@ open class Carousel: View { .pinLeadingGreaterThanOrEqualTo() .pinTrailing() .heightGreaterThanEqualTo(containerSize.height) + containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate() containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 0) containerLeadingConstraint?.activate() - - // add content stackview + + // Add content stackview containerView.addSubview(contentStackView) - // add scrollview + // Add scrollview scrollContainerView.addSubview(scrollView) scrollView.pinToSuperView() - // add pagination button icons + // Add pagination button icons scrollContainerView.addSubview(previousButton) previousButton .pinLeadingGreaterThanOrEqualTo() @@ -309,7 +311,7 @@ open class Carousel: View { .pinTrailingLessThanOrEqualTo() .pinCenterY() - // add scroll container view & carousel scrollbar + // Add scroll container view & carousel scrollbar contentStackView.addArrangedSubview(scrollContainerView) contentStackView.addArrangedSubview(carouselScrollBar) contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView) @@ -322,7 +324,8 @@ open class Carousel: View { addlisteners() } - + + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() @@ -353,7 +356,7 @@ open class Carousel: View { if peek == .none { paginationDisplay = .persistent } - + // Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard. if UIDevice.isIPad && peek == .minimum { peek = .standard @@ -363,11 +366,12 @@ open class Carousel: View { if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP { peek = .minimum } - + updatePaginationControls() addCarouselSlots() } + /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false @@ -384,19 +388,69 @@ open class Carousel: View { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- + private func addlisteners() { + nextButton.onClick = { _ in self.nextButtonClick() } + previousButton.onClick = { _ in self.previousButtonClick() } + + /// Will be called when the thumb move forward. + carouselScrollBar.onMoveForward = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onMoveForward") + } + + /// Will be called when the thumb move backward. + carouselScrollBar.onMoveBackward = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward") + } + + /// Will be called when the thumb touch start. + carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart") + } + + /// Will be called when the thumb touch end. + carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd") + } + } + + private func updatePaginationControls() { + containerView.surface = surface + showPaginationControls() + previousButton.kind = pagination.kind + previousButton.floating = pagination.floating + nextButton.kind = pagination.kind + nextButton.floating = pagination.floating + previousButton.surface = surface + nextButton.surface = surface + } + + private func showPaginationControls() { + if carouselScrollBar.numberOfSlides == _layout.value { + previousButton.isHidden = true + nextButton.isHidden = true + } else { + previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none) + nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none) + } + } + private func addCarouselSlots() { getSlotWidth() if containerView.frame.size.width > 0 { containerViewHeightConstraint?.isActive = false containerStackHeightConstraint?.isActive = false - // perform a loop to iterate each subView + // Perform a loop to iterate each subView scrollView.subviews.forEach { subView in - // removing subView from its parent view + // Removing subView from its parent view subView.removeFromSuperview() } - // add carousel items + // Add carousel items if data.count > 0 { var xPos = 0.0 for x in 0...data.count - 1 { @@ -433,86 +487,7 @@ open class Carousel: View { } } - private func ratioSize(for width: CGFloat) -> CGSize { - var height: CGFloat = width - - switch aspectRatio { - case .ratio1x1: - break; - case .ratio3x4: - height = (4 / 3) * width - case .ratio4x3: - height = (3 / 4) * width - case .ratio2x3: - height = (3 / 2) * width - case .ratio3x2: - height = (2 / 3) * width - case .ratio9x16: - height = (16 / 9) * width - case .ratio16x9: - height = (9 / 16) * width - case .ratio1x2: - height = (2 / 1) * width - case .ratio2x1: - height = (1 / 2) * width - - default: - break - } - - return CGSize(width: width, height: height) - } - - func addlisteners() { - nextButton.onClick = { _ in self.nextButtonClick() } - previousButton.onClick = { _ in self.previousButtonClick() } - - /// will be called when the thumb move forward. - carouselScrollBar.onMoveForward = { [weak self] scrubberId in - guard let self else { return } - updateScrollPosition(position: scrubberId, callbackText:"onMoveForward") - } - - /// will be called when the thumb move backward. - carouselScrollBar.onMoveBackward = { [weak self] scrubberId in - guard let self else { return } - updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward") - } - - /// will be called when the thumb touch start. - carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in - guard let self else { return } - updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart") - } - - /// will be called when the thumb touch end. - carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in - guard let self else { return } - updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd") - } - } - - func updatePaginationControls() { - containerView.surface = surface - showPaginationControls() - previousButton.kind = pagination.kind - previousButton.floating = pagination.floating - nextButton.kind = pagination.kind - nextButton.floating = pagination.floating - previousButton.surface = surface - nextButton.surface = surface - } - - func updatePaginationInset() { - prevButtonLeadingConstraint?.isActive = false - nextButtonTrailingConstraint?.isActive = false - prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset) - nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset) - prevButtonLeadingConstraint?.isActive = true - nextButtonTrailingConstraint?.isActive = true - } - - func getSlotWidth() { + private func getSlotWidth() { let actualWidth = containerView.frame.size.width let isScrollbarSuppressed = data.count > 0 && layout.value == data.count let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum @@ -538,19 +513,69 @@ open class Carousel: View { minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value) } - func nextButtonClick() { + private func nextButtonClick() { carouselScrollBar.position = carouselScrollBar.position+1 showPaginationControls() updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") } - func previousButtonClick() { + private func previousButtonClick() { carouselScrollBar.position = carouselScrollBar.position-1 showPaginationControls() updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") } - func updateScrollPosition(position: Int, callbackText: String) { + private func ratioSize(for width: CGFloat) -> CGSize { + var height: CGFloat = width + + switch aspectRatio { + case .ratio1x1: + break; + case .ratio3x4: + height = (4 / 3) * width + case .ratio4x3: + height = (3 / 4) * width + case .ratio2x3: + height = (3 / 2) * width + case .ratio3x2: + height = (2 / 3) * width + case .ratio9x16: + height = (16 / 9) * width + case .ratio16x9: + height = (9 / 16) * width + case .ratio1x2: + height = (2 / 1) * width + case .ratio2x1: + height = (1 / 2) * width + + default: + break + } + + return CGSize(width: width, height: height) + } + + private func updatePaginationInset() { + prevButtonLeadingConstraint?.isActive = false + nextButtonTrailingConstraint?.isActive = false + prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset) + nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset) + prevButtonLeadingConstraint?.isActive = true + nextButtonTrailingConstraint?.isActive = true + } + + private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) { + let scrollContentSizeWidth = scrollView.contentSize.width + let totalPositions = totalPositions() + let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions)))) + let remindSpace = Int(targetContentOffsetXPos) % layoutSpace + var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1 + contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos + carouselScrollBar.position = contentPos + updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved") + } + + private func updateScrollPosition(position: Int, callbackText: String) { if carouselScrollBar.numberOfSlides > 0 { let scrollContentSizeWidth = scrollView.contentSize.width let totalPositions = totalPositions() @@ -586,30 +611,9 @@ open class Carousel: View { } } - func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) { - let scrollContentSizeWidth = scrollView.contentSize.width - let totalPositions = totalPositions() - let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions)))) - let remindSpace = Int(targetContentOffsetXPos) % layoutSpace - var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1 - contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos - carouselScrollBar.position = contentPos - updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved") - } - private func totalPositions() -> Int { return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value))) } - - func showPaginationControls() { - if carouselScrollBar.numberOfSlides == _layout.value { - previousButton.isHidden = true - nextButton.isHidden = true - } else { - previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none) - nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none) - } - } } extension Carousel: UIScrollViewDelegate { @@ -619,6 +623,7 @@ extension Carousel: UIScrollViewDelegate { public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x) } + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { var visibleRect = CGRect() visibleRect.origin = scrollView.contentOffset From 77288e4c547cf8a2248912ec5bce506c62fbc604 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Thu, 4 Jul 2024 20:24:33 +0530 Subject: [PATCH 20/31] Digital ACT-191 ONEAPP-7013 story: slot alignment and rendering data --- VDS.xcodeproj/project.pbxproj | 10 +- VDS/Components/Carousel/Carousel.swift | 157 +++++++++++++++--- .../Carousel/CarouselRenderItemStyle.swift | 33 ++++ .../Carousel/CarouselSlotAlignmentModel.swift | 14 +- .../Carousel/CarouselSlotItemModel.swift | 31 ++++ 5 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 VDS/Components/Carousel/CarouselRenderItemStyle.swift create mode 100644 VDS/Components/Carousel/CarouselSlotItemModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 4c72a920..64ec60bb 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */; }; + 18013CEF2C355C5200907F18 /* CarouselRenderItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */; }; 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; }; @@ -19,11 +21,9 @@ 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; }; - 18AE87542C06FE610075F181 /* CarouselChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */; }; 18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; }; - 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; @@ -209,6 +209,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotItemModel.swift; sourceTree = ""; }; + 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselRenderItemStyle.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 = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; @@ -502,6 +504,8 @@ 18AE874F2C06FDA60075F181 /* Carousel.swift */, 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */, 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */, + 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */, + 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */, 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, ); path = Carousel; @@ -1363,6 +1367,7 @@ EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, 18AE87502C06FDA60075F181 /* Carousel.swift in Sources */, + 18013CEF2C355C5200907F18 /* CarouselRenderItemStyle.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */, EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */, @@ -1405,6 +1410,7 @@ EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */, EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */, + 18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */, 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */, EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */, 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index dbc10d98..a3cb1f1c 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -33,21 +33,6 @@ open class Carousel: View { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- - /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. - public enum Gutter: String, CaseIterable { - case twelvePX = "12px" - case twentyFourPX = "24px" - - var value: CGFloat { - switch self { - case .twelvePX: - VDSLayout.space3X - case .twentyFourPX: - VDSLayout.space6X - } - } - } - /// Enum used to describe the pagination display for this component. public enum PaginationDisplay: String, CaseIterable { case persistent, none @@ -75,6 +60,21 @@ open class Carousel: View { case value(CGFloat) } + /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. + public enum Gutter: String, CaseIterable { + case twelvePX = "12px" + case twentyFourPX = "24px" + + var value: CGFloat { + switch self { + case .twelvePX: + VDSLayout.space3X + case .twentyFourPX: + VDSLayout.space6X + } + } + } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -123,11 +123,12 @@ open class Carousel: View { get { return _layout } set { _layout = newValue + carouselScrollBar.position = 0 setNeedsUpdate() } } - /// A callback when moving the carousel. Returns event object and selectedGroupIndex. + /// A callback when moving the carousel. Returns initial visible slide's index in the carousel. open var onChange: ((Int) -> Void)? { get { nil } set { @@ -182,7 +183,42 @@ open class Carousel: View { open var selectedIndex: Int? { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. - open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } + open var slotAlignment: CarouselSlotAlignmentModel? { + get { return _slotAlignment } + set { + if let newValue { + _slotAlignment = newValue + } else { + _slotAlignment = nil + } + setNeedsUpdate() + } + } + + /// Render item style. If provided, the slot gets the background, width, height, border-radius. + open var renderItemStyle: CarouselRenderItemStyle? { + get { return _renderItemStyle } + set { + if let newValue { + _renderItemStyle = newValue + } else { + _renderItemStyle = nil + } + setNeedsUpdate() + } + } + + /// Render item. It passes a data array object and expects the styled component to apply in return. + open var renderItem: CarouselSlotItemModel? { + get { _renderItem } + set { + if let newValue { + _renderItem = newValue + } else { + _renderItem = nil + } + } + } //-------------------------------------------------- // MARK: - Private Properties @@ -250,9 +286,12 @@ open class Carousel: View { internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX internal var _peek: Peek = .standard internal var _numberOfSlides: Int = 1 + internal var _slotAlignment: CarouselSlotAlignmentModel? = nil + internal var _renderItemStyle: CarouselRenderItemStyle? = nil + internal var _renderItem: CarouselSlotItemModel? = nil private var _width: Width? = nil - private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } + private var selectedGroupIndex: Int? = nil private var containerStackHeightConstraint: NSLayoutConstraint? private var containerViewHeightConstraint: NSLayoutConstraint? private var prevButtonLeadingConstraint: NSLayoutConstraint? @@ -323,6 +362,7 @@ open class Carousel: View { .heightGreaterThanEqualTo(containerSize.height) addlisteners() + updatePaginationInset() } /// Used to make changes to the View based off a change events or from local properties. @@ -348,7 +388,9 @@ open class Carousel: View { carouselScrollBar.numberOfSlides = data.count carouselScrollBar.layout = _layout - carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position + if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) { + carouselScrollBar.position = 1 + } carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false // Mobile/Tablet layouts without peek - must show pagination controls. @@ -392,31 +434,32 @@ open class Carousel: View { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } - /// Will be called when the thumb move forward. + /// Will be called when the scrollbar thumb move forward. carouselScrollBar.onMoveForward = { [weak self] scrubberId in guard let self else { return } updateScrollPosition(position: scrubberId, callbackText:"onMoveForward") } - /// Will be called when the thumb move backward. + /// Will be called when the scrollbar thumb move backward. carouselScrollBar.onMoveBackward = { [weak self] scrubberId in guard let self else { return } updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward") } - /// Will be called when the thumb touch start. + /// Will be called when the scrollbar thumb touch start. carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in guard let self else { return } updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart") } - /// Will be called when the thumb touch end. + /// Will be called when the scrollbar thumb touch end. carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in guard let self else { return } updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd") } } + // Update pagination buttons with selected surface, kind, floating values private func updatePaginationControls() { containerView.surface = surface showPaginationControls() @@ -428,6 +471,7 @@ open class Carousel: View { nextButton.surface = surface } + // Show/Hide pagination buttons of Carousel based on First or Middle or Last private func showPaginationControls() { if carouselScrollBar.numberOfSlides == _layout.value { previousButton.isHidden = true @@ -438,6 +482,7 @@ open class Carousel: View { } } + // Add carousel slots and load data if any private func addCarouselSlots() { getSlotWidth() if containerView.frame.size.width > 0 { @@ -454,6 +499,8 @@ open class Carousel: View { if data.count > 0 { var xPos = 0.0 for x in 0...data.count - 1 { + + // Add Carousel Slot let carouselSlot = View().with { $0.clipsToBounds = true $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) @@ -470,6 +517,29 @@ open class Carousel: View { .height(slotHeight) carouselSlot.layer.cornerRadius = 12.0 xPos = xPos + minimumSlotWidth + gutter.value + + // Add subview for content to Carousel Slot + let contentView = View().with { + $0.clipsToBounds = true + $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) + } + carouselSlot.addSubview(contentView) + + // Add received component + let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: data[x] as? UIView) + if let component = item.component { + if slotAlignment != nil { + setSlotAlignment(contentView: contentView, parentView: carouselSlot) + } else { + contentView.pinToSuperView() + } + contentView.addSubview(component) + component.pinToSuperView() + contentView.layer.cornerRadius = component.layer.cornerRadius + if var surfacedView = component as? Surfaceable { + surfacedView.surface = surface + } + } } scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) } @@ -487,6 +557,37 @@ open class Carousel: View { } } + // Set slot alignment if provided. Used only when slot content have different heights or widths. + private func setSlotAlignment(contentView: View, parentView: View) { + parentView.backgroundColor = .clear + switch slotAlignment?.vertical { + case .top: + contentView.topAnchor.constraint(equalTo: parentView.topAnchor).activate() + break + case .middle: + contentView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor).activate() + break + case .bottom: + contentView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).activate() + break + default: break + } + + switch slotAlignment?.horizontal { + case .left: + contentView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor).activate() + break + case .center: + contentView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor).activate() + break + case .right: + parentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).activate() + break + default: break + } + } + + // Get the slot width relative to the peak private func getSlotWidth() { let actualWidth = containerView.frame.size.width let isScrollbarSuppressed = data.count > 0 && layout.value == data.count @@ -505,7 +606,7 @@ open class Carousel: View { case .minimum: // Peek Mimumum Width: 24px from edge of container (at the default view of the carousel with one peek visible) // Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard. - minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum + minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum - gutter.value case .none: break } @@ -525,6 +626,7 @@ open class Carousel: View { updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") } + // The size of slot depends on the selected aspect ratio private func ratioSize(for width: CGFloat) -> CGSize { var height: CGFloat = width @@ -575,6 +677,7 @@ open class Carousel: View { updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved") } + // Update scrollview offset relative to scrollbar thumb position private func updateScrollPosition(position: Int, callbackText: String) { if carouselScrollBar.numberOfSlides > 0 { let scrollContentSizeWidth = scrollView.contentSize.width @@ -598,7 +701,7 @@ open class Carousel: View { xPos = xPosition - gutter.value - (minimumSlotWidth/4)/2 } case .minimum: - xPos = isPeekMinimumOnTablet ? xPosition : xPosition - peekMinimum/2 + xPos = isPeekMinimumOnTablet ? xPosition : xPosition - peekMinimum case .none: xPos = xPosition } @@ -608,9 +711,13 @@ open class Carousel: View { let yPos = scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) showPaginationControls() + selectedIndex = ((position-1) * layout.value) + 1 + onChangePublisher.send(selectedIndex ?? 1) + selectedGroupIndex = position } } + // Get the overall positions of the carousel scrollbar relative to the slides and selected layout private func totalPositions() -> Int { return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value))) } diff --git a/VDS/Components/Carousel/CarouselRenderItemStyle.swift b/VDS/Components/Carousel/CarouselRenderItemStyle.swift new file mode 100644 index 00000000..c447dfa1 --- /dev/null +++ b/VDS/Components/Carousel/CarouselRenderItemStyle.swift @@ -0,0 +1,33 @@ +// +// CarouselRenderItemStyle.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 30/06/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +/// A custom data type that holds the style props if provided any. +public struct CarouselRenderItemStyle { + + /// BackgroundColor for slot + public let backgroundColor: String? + + /// Height for slot + public var height: CGFloat? + + /// BorderRadius for slot + public var borderRadius: CGFloat? + + /// Width for slot + public var width: CGFloat? + + public init(backgroundColor: String?, height: CGFloat?, width: CGFloat?, borderRadius: CGFloat?) { + self.backgroundColor = backgroundColor + self.height = height + self.borderRadius = borderRadius ?? 12.0 + self.width = width + } +} diff --git a/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift b/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift index 5a8ec661..ee1d491b 100644 --- a/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift +++ b/VDS/Components/Carousel/CarouselSlotAlignmentModel.swift @@ -7,17 +7,19 @@ import Foundation -/// Custom data type for slotAlignment prop for 'Carousel' component. +/// Custom data type for the SlotAlignment prop for the 'carousel' component. extension Carousel { + + /// Used only when slot content have different heights or widths. public struct CarouselSlotAlignmentModel { - /// Text that shown to an indicator for legend - public var vertical: String + /// Used for vertical alignment of slot alignment. + public var vertical: Carousel.Vertical - /// Date to an indicator - public var horizontal: String + /// Used for horizontal alignment of slot alignment. + public var horizontal: Carousel.Horizontal - public init(vertical: String, horizontal: String) { + public init(vertical: Carousel.Vertical, horizontal: Carousel.Horizontal) { self.vertical = vertical self.horizontal = horizontal } diff --git a/VDS/Components/Carousel/CarouselSlotItemModel.swift b/VDS/Components/Carousel/CarouselSlotItemModel.swift new file mode 100644 index 00000000..3830b29f --- /dev/null +++ b/VDS/Components/Carousel/CarouselSlotItemModel.swift @@ -0,0 +1,31 @@ +// +// CarouselSlotItemModel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 30/06/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +/// A custom data type that holds the style and component for a slot of the 'Carousel' component. +public struct CarouselSlotItemModel { + + /// Style props if provided any + public var style: CarouselRenderItemStyle? + + /// Component to be show on Carousel slot + public var component: UIView? + + public init(style: CarouselRenderItemStyle? = nil, component: UIView? = nil) { + self.style = style + self.component = component + if let color = style?.backgroundColor { + self.component?.backgroundColor = .init(hexString: color) + } + if let borderRadius = style?.borderRadius { + self.component?.layer.cornerRadius = borderRadius + } + } +} From 928db0f1fc3e5bb69bf698442d3fe962997125bd Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Wed, 17 Jul 2024 18:32:04 +0530 Subject: [PATCH 21/31] Digital ACT-191 ONEAPP-9311 story: changes about PR notes --- VDS/Components/Carousel/Carousel.swift | 193 +++++------------- .../Carousel/CarouselSlotItemModel.swift | 4 +- 2 files changed, 58 insertions(+), 139 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index a3cb1f1c..90b35588 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -78,14 +78,11 @@ open class Carousel: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - /// Aspect-ratio options for tilelet in the carousel. If 'none' is passed, the tilelet will take the height of the tallest item in the carousel. - open var aspectRatio: Tilelet.AspectRatio = .none { didSet { setNeedsUpdate() } } - - /// Data used to render tilelets in the carousel. - open var data: [Any] = [] { didSet { setNeedsUpdate() } } + /// views used to render view in the carousel slots. + open var views: [UIView] = [] { didSet { setNeedsUpdate() } } /// If provided, width of slots will be rendered based on this value. If omitted, default widths are rendered. - open var width : Width? { + open var width: Width? { get { _width } set { if let newValue { @@ -110,19 +107,11 @@ open class Carousel: View { } /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. - open var gutter: Gutter { - get { return _gutter } - set { - _gutter = newValue - setNeedsUpdate() - } - } + open var gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX { didSet { setNeedsUpdate() } } /// The amount of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. - open var layout: CarouselScrollbar.Layout { - get { return _layout } - set { - _layout = newValue + open var layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP { + didSet { carouselScrollBar.position = 0 setNeedsUpdate() } @@ -143,82 +132,29 @@ open class Carousel: View { } /// Config object for pagination. - open var pagination: CarouselPaginationModel { - get { return _pagination } - set { - _pagination = newValue - setNeedsUpdate() - } - } + open var pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) { didSet {setNeedsUpdate() } } /// If provided, will determine the conditions to render the pagination arrows. - open var paginationDisplay: PaginationDisplay { - get { return _paginationDisplay } - set { - _paginationDisplay = newValue - setNeedsUpdate() - } - } + open var paginationDisplay: PaginationDisplay = .none { didSet {setNeedsUpdate() } } /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. /// The default value will be 12px in tablet and 8px in mobile. These values are the default in order to avoid overlapping content within the carousel. - open var paginationInset: CGFloat { - get { return _paginationInset } - set { - _paginationInset = newValue - updatePaginationInset() - } - } + open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } } /// Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. - open var peek: Peek { - get { return _peek } - set { - _peek = newValue - setNeedsUpdate() - } - } + open var peek: Peek = .standard { didSet { setNeedsUpdate() } } /// The initial visible slide's index in the carousel. open var selectedIndex: Int? { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. - open var slotAlignment: CarouselSlotAlignmentModel? { - get { return _slotAlignment } - set { - if let newValue { - _slotAlignment = newValue - } else { - _slotAlignment = nil - } - setNeedsUpdate() - } - } + open var slotAlignment: CarouselSlotAlignmentModel? = nil { didSet { setNeedsUpdate() } } /// Render item style. If provided, the slot gets the background, width, height, border-radius. - open var renderItemStyle: CarouselRenderItemStyle? { - get { return _renderItemStyle } - set { - if let newValue { - _renderItemStyle = newValue - } else { - _renderItemStyle = nil - } - setNeedsUpdate() - } - } + open var renderItemStyle: CarouselRenderItemStyle? = nil { didSet { setNeedsUpdate() } } - /// Render item. It passes a data array object and expects the styled component to apply in return. - open var renderItem: CarouselSlotItemModel? { - get { _renderItem } - set { - if let newValue { - _renderItem = newValue - } else { - _renderItem = nil - } - } - } +// /// Render item. It passes a data array object and expects the styled component to apply in return. +// open var renderItem: CarouselSlotItemModel? = nil { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties @@ -278,18 +214,6 @@ open class Carousel: View { /// A publisher for when the carousel moves. Passes parameters (data). open var onScrollPublisher = PassthroughSubject, Never>() private var onScrollCancellable: AnyCancellable? - - internal var _layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP - internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) - internal var _paginationDisplay: PaginationDisplay = .none - internal var _paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X - internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX - internal var _peek: Peek = .standard - internal var _numberOfSlides: Int = 1 - internal var _slotAlignment: CarouselSlotAlignmentModel? = nil - internal var _renderItemStyle: CarouselRenderItemStyle? = nil - internal var _renderItem: CarouselSlotItemModel? = nil - private var _width: Width? = nil private var selectedGroupIndex: Int? = nil private var containerStackHeightConstraint: NSLayoutConstraint? @@ -301,7 +225,7 @@ open class Carousel: View { // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X - var slotHeight = 100.0 + var slotHeight = 50.0 var peekMinimum = 24.0 var minimumSlotWidth = 0.0 var carouselScrollbarMinWidth = 96.0 @@ -386,8 +310,8 @@ open class Carousel: View { } } - carouselScrollBar.numberOfSlides = data.count - carouselScrollBar.layout = _layout + carouselScrollBar.numberOfSlides = views.count + carouselScrollBar.layout = layout if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) { carouselScrollBar.position = 1 } @@ -417,7 +341,6 @@ open class Carousel: View { open override func reset() { super.reset() shouldUpdateView = false - aspectRatio = .none layout = UIDevice.isIPad ? .threeUP : .oneUP pagination = .init(kind: .lowContrast, floating: true) paginationDisplay = .none @@ -473,7 +396,7 @@ open class Carousel: View { // Show/Hide pagination buttons of Carousel based on First or Middle or Last private func showPaginationControls() { - if carouselScrollBar.numberOfSlides == _layout.value { + if carouselScrollBar.numberOfSlides == layout.value { previousButton.isHidden = true nextButton.isHidden = true } else { @@ -482,6 +405,26 @@ open class Carousel: View { } } + private func estimateHeightFor(item: CarouselSlotItemModel, with width: CGFloat) -> CGFloat { + let itemWidth = width + let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude) + let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight) + return estItemSize.height + } + + private func fetchCarouselHeight() { + if views.count > 0 { + var height = slotHeight + for x in 0...views.count - 1 { + // Add received component + let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: views[x]) + slotHeight = estimateHeightFor(item: item, with: minimumSlotWidth) + height = slotHeight > height ? slotHeight : height + } + slotHeight = height + } + } + // Add carousel slots and load data if any private func addCarouselSlots() { getSlotWidth() @@ -494,11 +437,13 @@ open class Carousel: View { // Removing subView from its parent view subView.removeFromSuperview() } + + fetchCarouselHeight() // Add carousel items - if data.count > 0 { + if views.count > 0 { var xPos = 0.0 - for x in 0...data.count - 1 { + for x in 0...views.count - 1 { // Add Carousel Slot let carouselSlot = View().with { @@ -507,8 +452,7 @@ open class Carousel: View { } scrollView.addSubview(carouselSlot) scrollView.delegate = self - let size = ratioSize(for: minimumSlotWidth) - slotHeight = size.height + carouselSlot .pinTop() .pinBottom() @@ -518,24 +462,28 @@ open class Carousel: View { carouselSlot.layer.cornerRadius = 12.0 xPos = xPos + minimumSlotWidth + gutter.value + // Add received component + let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: views[x]) + let contentViewHeight = estimateHeightFor(item: item, with: minimumSlotWidth) + // Add subview for content to Carousel Slot let contentView = View().with { $0.clipsToBounds = true - $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) } carouselSlot.addSubview(contentView) - - // Add received component - let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: data[x] as? UIView) + if let component = item.component { if slotAlignment != nil { + // If slotAlignment exist, should use expected height + contentView.widthAnchor.constraint(equalToConstant: minimumSlotWidth).activate() + contentView.heightAnchor.constraint(equalToConstant: contentViewHeight).activate() setSlotAlignment(contentView: contentView, parentView: carouselSlot) } else { +// // If no slotAlignment, should use full slot contentView.pinToSuperView() } contentView.addSubview(component) component.pinToSuperView() - contentView.layer.cornerRadius = component.layer.cornerRadius if var surfacedView = component as? Surfaceable { surfacedView.surface = surface } @@ -590,7 +538,7 @@ open class Carousel: View { // Get the slot width relative to the peak private func getSlotWidth() { let actualWidth = containerView.frame.size.width - let isScrollbarSuppressed = data.count > 0 && layout.value == data.count + let isScrollbarSuppressed = views.count > 0 && layout.value == views.count let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum let isPeekNone: Bool = peek == .none minimumSlotWidth = isScrollbarSuppressed || isPeekMinimumOnTablet || isPeekNone ? actualWidth - ((CGFloat(layout.value)-1) * gutter.value): actualWidth - (CGFloat(layout.value) * gutter.value) @@ -626,37 +574,6 @@ open class Carousel: View { updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks") } - // The size of slot depends on the selected aspect ratio - private func ratioSize(for width: CGFloat) -> CGSize { - var height: CGFloat = width - - switch aspectRatio { - case .ratio1x1: - break; - case .ratio3x4: - height = (4 / 3) * width - case .ratio4x3: - height = (3 / 4) * width - case .ratio2x3: - height = (3 / 2) * width - case .ratio3x2: - height = (2 / 3) * width - case .ratio9x16: - height = (16 / 9) * width - case .ratio16x9: - height = (9 / 16) * width - case .ratio1x2: - height = (2 / 1) * width - case .ratio2x1: - height = (1 / 2) * width - - default: - break - } - - return CGSize(width: width, height: height) - } - private func updatePaginationInset() { prevButtonLeadingConstraint?.isActive = false nextButtonTrailingConstraint?.isActive = false @@ -688,7 +605,7 @@ open class Carousel: View { } else if position == totalPositions { xPos = scrollContentSizeWidth - containerView.frame.size.width } else { - let isScrollbarSuppressed = data.count > 0 && layout.value == data.count + let isScrollbarSuppressed = views.count > 0 && layout.value == views.count let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum if !isScrollbarSuppressed { let subpart = minimumSlotWidth + gutter.value @@ -719,7 +636,7 @@ open class Carousel: View { // Get the overall positions of the carousel scrollbar relative to the slides and selected layout private func totalPositions() -> Int { - return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value))) + return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(layout.value))) } } diff --git a/VDS/Components/Carousel/CarouselSlotItemModel.swift b/VDS/Components/Carousel/CarouselSlotItemModel.swift index 3830b29f..a52192c4 100644 --- a/VDS/Components/Carousel/CarouselSlotItemModel.swift +++ b/VDS/Components/Carousel/CarouselSlotItemModel.swift @@ -14,7 +14,9 @@ public struct CarouselSlotItemModel { /// Style props if provided any public var style: CarouselRenderItemStyle? - + + public let defaultHeight: CGFloat = 50.0 + /// Component to be show on Carousel slot public var component: UIView? From 4ae07b640246a241291fdd7ded7dac02915c5dd2 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Thu, 18 Jul 2024 12:25:04 +0530 Subject: [PATCH 22/31] Digital ACT-191 ONEAPP-7013 story: removing width prop, and passing selectedGroupIndex through onChange --- VDS/Components/Carousel/Carousel.swift | 96 ++++--------------- .../Carousel/CarouselSlotItemModel.swift | 12 +-- 2 files changed, 20 insertions(+), 88 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 90b35588..5595ef68 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -81,31 +81,6 @@ open class Carousel: View { /// views used to render view in the carousel slots. open var views: [UIView] = [] { didSet { setNeedsUpdate() } } - /// If provided, width of slots will be rendered based on this value. If omitted, default widths are rendered. - open var width: Width? { - get { _width } - set { - if let newValue { - switch newValue { - case .percentage(let percentage): - if percentage >= 10 && percentage <= 100.0 { - let expectedWidth = safeAreaLayoutGuide.layoutFrame.size.width * (percentage/100) - if expectedWidth > carouselScrollbarMinWidth { - _width = newValue - } - } - case .value(let value): - if value > carouselScrollbarMinWidth { - _width = newValue - } - } - } else { - _width = nil - } - setNeedsUpdate() - } - } - /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. open var gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX { didSet { setNeedsUpdate() } } @@ -117,7 +92,7 @@ open class Carousel: View { } } - /// A callback when moving the carousel. Returns initial visible slide's index in the carousel. + /// A callback when moving the carousel. Returns selectedGroupIndex. open var onChange: ((Int) -> Void)? { get { nil } set { @@ -150,12 +125,6 @@ open class Carousel: View { /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: CarouselSlotAlignmentModel? = nil { didSet { setNeedsUpdate() } } - /// Render item style. If provided, the slot gets the background, width, height, border-radius. - open var renderItemStyle: CarouselRenderItemStyle? = nil { didSet { setNeedsUpdate() } } - -// /// Render item. It passes a data array object and expects the styled component to apply in return. -// open var renderItem: CarouselSlotItemModel? = nil { didSet { setNeedsUpdate() } } - //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -207,15 +176,10 @@ open class Carousel: View { $0.icon.customSize = UIDevice.isIPad ? 16 : 12 } - /// A publisher for when the scrubber position changes. Passes parameters (position). + /// A publisher for when moving the carousel. Passes parameters selectedGroupIndex (position). open var onChangePublisher = PassthroughSubject() private var onChangeCancellable: AnyCancellable? - /// A publisher for when the carousel moves. Passes parameters (data). - open var onScrollPublisher = PassthroughSubject, Never>() - private var onScrollCancellable: AnyCancellable? - private var _width: Width? = nil - private var selectedGroupIndex: Int? = nil private var containerStackHeightConstraint: NSLayoutConstraint? private var containerViewHeightConstraint: NSLayoutConstraint? private var prevButtonLeadingConstraint: NSLayoutConstraint? @@ -225,7 +189,7 @@ open class Carousel: View { // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X - var slotHeight = 50.0 + var slotDefaultHeight = 50.0 var peekMinimum = 24.0 var minimumSlotWidth = 0.0 var carouselScrollbarMinWidth = 96.0 @@ -248,13 +212,10 @@ open class Carousel: View { containerView .pinTop() .pinBottom() - .pinLeadingGreaterThanOrEqualTo() + .pinLeading() .pinTrailing() .heightGreaterThanEqualTo(containerSize.height) - containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate() - containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 0) - containerLeadingConstraint?.activate() // Add content stackview containerView.addSubview(contentStackView) @@ -293,23 +254,6 @@ open class Carousel: View { open override func updateView() { super.updateView() - if containerView.frame.size.width > 0 { - if let width { - containerLeadingConstraint?.deactivate() - switch width { - case .value(let value): - var expectedWidth = value - let fullWidth = safeAreaLayoutGuide.layoutFrame.size.width - expectedWidth = expectedWidth > fullWidth ? fullWidth : expectedWidth - containerLeadingConstraint?.constant = safeAreaLayoutGuide.layoutFrame.size.width - expectedWidth - case .percentage(let percentage): - let expectedWidth = safeAreaLayoutGuide.layoutFrame.size.width * (percentage/100) - containerLeadingConstraint?.constant = safeAreaLayoutGuide.layoutFrame.size.width - expectedWidth - } - containerLeadingConstraint?.activate() - } - } - carouselScrollBar.numberOfSlides = views.count carouselScrollBar.layout = layout if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) { @@ -347,7 +291,6 @@ open class Carousel: View { paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX peek = .standard - width = nil } //-------------------------------------------------- @@ -412,17 +355,16 @@ open class Carousel: View { return estItemSize.height } - private func fetchCarouselHeight() { + private func fetchCarouselHeight() -> CGFloat { + var height = slotDefaultHeight if views.count > 0 { - var height = slotHeight for x in 0...views.count - 1 { - // Add received component - let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: views[x]) - slotHeight = estimateHeightFor(item: item, with: minimumSlotWidth) - height = slotHeight > height ? slotHeight : height + let item : CarouselSlotItemModel = .init(component: views[x]) + let estHeight = estimateHeightFor(item: item, with: minimumSlotWidth) + height = estHeight > height ? estHeight : height } - slotHeight = height } + return height } // Add carousel slots and load data if any @@ -431,14 +373,13 @@ open class Carousel: View { if containerView.frame.size.width > 0 { containerViewHeightConstraint?.isActive = false containerStackHeightConstraint?.isActive = false + let slotHeight = fetchCarouselHeight() // Perform a loop to iterate each subView scrollView.subviews.forEach { subView in // Removing subView from its parent view subView.removeFromSuperview() } - - fetchCarouselHeight() // Add carousel items if views.count > 0 { @@ -452,7 +393,7 @@ open class Carousel: View { } scrollView.addSubview(carouselSlot) scrollView.delegate = self - + carouselSlot .pinTop() .pinBottom() @@ -463,7 +404,7 @@ open class Carousel: View { xPos = xPos + minimumSlotWidth + gutter.value // Add received component - let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: views[x]) + let item : CarouselSlotItemModel = .init(component: views[x]) let contentViewHeight = estimateHeightFor(item: item, with: minimumSlotWidth) // Add subview for content to Carousel Slot @@ -471,7 +412,7 @@ open class Carousel: View { $0.clipsToBounds = true } carouselSlot.addSubview(contentView) - + if let component = item.component { if slotAlignment != nil { // If slotAlignment exist, should use expected height @@ -479,12 +420,15 @@ open class Carousel: View { contentView.heightAnchor.constraint(equalToConstant: contentViewHeight).activate() setSlotAlignment(contentView: contentView, parentView: carouselSlot) } else { -// // If no slotAlignment, should use full slot + // If no slotAlignment, should use full slot contentView.pinToSuperView() } + carouselSlot.backgroundColor = .clear + carouselSlot.layer.cornerRadius = 0 contentView.addSubview(component) component.pinToSuperView() if var surfacedView = component as? Surfaceable { + contentView.surface = surface surfacedView.surface = surface } } @@ -628,9 +572,7 @@ open class Carousel: View { let yPos = scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) showPaginationControls() - selectedIndex = ((position-1) * layout.value) + 1 - onChangePublisher.send(selectedIndex ?? 1) - selectedGroupIndex = position + onChangePublisher.send(position-1) } } diff --git a/VDS/Components/Carousel/CarouselSlotItemModel.swift b/VDS/Components/Carousel/CarouselSlotItemModel.swift index a52192c4..3cb7240a 100644 --- a/VDS/Components/Carousel/CarouselSlotItemModel.swift +++ b/VDS/Components/Carousel/CarouselSlotItemModel.swift @@ -11,23 +11,13 @@ import VDSCoreTokens /// A custom data type that holds the style and component for a slot of the 'Carousel' component. public struct CarouselSlotItemModel { - - /// Style props if provided any - public var style: CarouselRenderItemStyle? public let defaultHeight: CGFloat = 50.0 /// Component to be show on Carousel slot public var component: UIView? - public init(style: CarouselRenderItemStyle? = nil, component: UIView? = nil) { - self.style = style + public init(component: UIView? = nil) { self.component = component - if let color = style?.backgroundColor { - self.component?.backgroundColor = .init(hexString: color) - } - if let borderRadius = style?.borderRadius { - self.component?.layer.cornerRadius = borderRadius - } } } From c2ec941a3cb4ef0d9be861774b8eb84ca1159dc4 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Thu, 18 Jul 2024 12:49:12 +0530 Subject: [PATCH 23/31] Digital ACT-191 ONEAPP-7013 story: updating carousel position if receives selectedIndex --- VDS/Components/Carousel/Carousel.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 5595ef68..35357778 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -279,6 +279,12 @@ open class Carousel: View { updatePaginationControls() addCarouselSlots() + + // If selectedIndex is received, the carousel should update its position. + if let selectedIndex { + let totalPos = totalPositions() + carouselScrollBar.position = selectedIndex >= totalPos ? totalPos : selectedIndex+1 + } } /// Resets to default settings. @@ -300,6 +306,12 @@ open class Carousel: View { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } + /// Will be called when the scrubber position changes. + carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange") + } + /// Will be called when the scrollbar thumb move forward. carouselScrollBar.onMoveForward = { [weak self] scrubberId in guard let self else { return } From 559423f629110d992625fd294cbe3e9c95345933 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Thu, 18 Jul 2024 13:22:41 +0530 Subject: [PATCH 24/31] Digital ACT-191 ONEAPP-7013 story: removed unused file --- VDS.xcodeproj/project.pbxproj | 4 --- .../Carousel/CarouselRenderItemStyle.swift | 33 ------------------- 2 files changed, 37 deletions(-) delete mode 100644 VDS/Components/Carousel/CarouselRenderItemStyle.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 64ec60bb..e1bf1b05 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */; }; - 18013CEF2C355C5200907F18 /* CarouselRenderItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */; }; 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; }; @@ -210,7 +209,6 @@ /* Begin PBXFileReference section */ 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotItemModel.swift; sourceTree = ""; }; - 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselRenderItemStyle.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 = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; @@ -505,7 +503,6 @@ 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */, 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */, 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */, - 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */, 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, ); path = Carousel; @@ -1367,7 +1364,6 @@ EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, 18AE87502C06FDA60075F181 /* Carousel.swift in Sources */, - 18013CEF2C355C5200907F18 /* CarouselRenderItemStyle.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */, EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */, diff --git a/VDS/Components/Carousel/CarouselRenderItemStyle.swift b/VDS/Components/Carousel/CarouselRenderItemStyle.swift deleted file mode 100644 index c447dfa1..00000000 --- a/VDS/Components/Carousel/CarouselRenderItemStyle.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// CarouselRenderItemStyle.swift -// VDS -// -// Created by Kanamarlapudi, Vasavi on 30/06/24. -// - -import Foundation -import UIKit -import VDSCoreTokens - -/// A custom data type that holds the style props if provided any. -public struct CarouselRenderItemStyle { - - /// BackgroundColor for slot - public let backgroundColor: String? - - /// Height for slot - public var height: CGFloat? - - /// BorderRadius for slot - public var borderRadius: CGFloat? - - /// Width for slot - public var width: CGFloat? - - public init(backgroundColor: String?, height: CGFloat?, width: CGFloat?, borderRadius: CGFloat?) { - self.backgroundColor = backgroundColor - self.height = height - self.borderRadius = borderRadius ?? 12.0 - self.width = width - } -} From 93004f6e1205114d60c6c1fee52d7b0064912f2e Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Thu, 18 Jul 2024 17:28:40 +0530 Subject: [PATCH 25/31] Digital ACT-191 ONEAPP-7013 story: Refactored code, removing redundant view and adding component to slot. --- VDS/Components/Carousel/Carousel.swift | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 35357778..9a9b3251 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -417,32 +417,29 @@ open class Carousel: View { // Add received component let item : CarouselSlotItemModel = .init(component: views[x]) - let contentViewHeight = estimateHeightFor(item: item, with: minimumSlotWidth) - - // Add subview for content to Carousel Slot - let contentView = View().with { - $0.clipsToBounds = true - } - carouselSlot.addSubview(contentView) - if let component = item.component { + carouselSlot.addSubview(component) if slotAlignment != nil { - // If slotAlignment exist, should use expected height - contentView.widthAnchor.constraint(equalToConstant: minimumSlotWidth).activate() - contentView.heightAnchor.constraint(equalToConstant: contentViewHeight).activate() - setSlotAlignment(contentView: contentView, parentView: carouselSlot) + // If slotAlignment exist, use component's own sizes + component.widthAnchor.constraint(lessThanOrEqualTo: carouselSlot.widthAnchor).activate() + setSlotAlignment(contentView: component, parentView: carouselSlot) } else { // If no slotAlignment, should use full slot - contentView.pinToSuperView() + component.pinToSuperView() } carouselSlot.backgroundColor = .clear carouselSlot.layer.cornerRadius = 0 - contentView.addSubview(component) - component.pinToSuperView() if var surfacedView = component as? Surfaceable { - contentView.surface = surface surfacedView.surface = surface } + + // TO BE REMOVED AFTER VQA - borderWidth and borderColor below did set only for Testing purpose to VQA + carouselSlot.layer.borderWidth = 1.0 + carouselSlot.layer.borderColor = SurfaceColorConfiguration().with { + $0.lightColor = VDSColor.elementsPrimaryOnlight + $0.darkColor = VDSColor.elementsPrimaryOndark + }.getColor(self).cgColor + } } scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) @@ -462,7 +459,7 @@ open class Carousel: View { } // Set slot alignment if provided. Used only when slot content have different heights or widths. - private func setSlotAlignment(contentView: View, parentView: View) { + private func setSlotAlignment(contentView: UIView, parentView: View) { parentView.backgroundColor = .clear switch slotAlignment?.vertical { case .top: From 076f4c082cb6aeb2b68be8137fc50c3471a4714c Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Thu, 18 Jul 2024 20:26:06 +0530 Subject: [PATCH 26/31] Digital ACT-191 ONEAPP-7013 story: removed border, background color to slot, removed unused file, updated slotAlignment method. --- VDS.xcodeproj/project.pbxproj | 4 - VDS/Components/Carousel/Carousel.swift | 80 +++++++------------ .../Carousel/CarouselSlotItemModel.swift | 23 ------ 3 files changed, 30 insertions(+), 77 deletions(-) delete mode 100644 VDS/Components/Carousel/CarouselSlotItemModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 93331c0e..59600512 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */; }; 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; }; @@ -208,7 +207,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotItemModel.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 = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; @@ -502,7 +500,6 @@ 18AE874F2C06FDA60075F181 /* Carousel.swift */, 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */, 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */, - 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */, 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, ); path = Carousel; @@ -1406,7 +1403,6 @@ EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */, EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */, - 18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */, 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */, EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */, 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 9a9b3251..0a6e71f9 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -123,7 +123,7 @@ open class Carousel: View { open var selectedIndex: Int? { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. - open var slotAlignment: CarouselSlotAlignmentModel? = nil { didSet { setNeedsUpdate() } } + open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties @@ -360,19 +360,17 @@ open class Carousel: View { } } - private func estimateHeightFor(item: CarouselSlotItemModel, with width: CGFloat) -> CGFloat { - let itemWidth = width + private func estimateHeightFor(component: UIView, with itemWidth: CGFloat) -> CGFloat { let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude) - let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight) + let estItemSize = component.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) return estItemSize.height } private func fetchCarouselHeight() -> CGFloat { var height = slotDefaultHeight if views.count > 0 { - for x in 0...views.count - 1 { - let item : CarouselSlotItemModel = .init(component: views[x]) - let estHeight = estimateHeightFor(item: item, with: minimumSlotWidth) + for index in 0...views.count - 1 { + let estHeight = estimateHeightFor(component: views[index], with: minimumSlotWidth) height = estHeight > height ? estHeight : height } } @@ -396,12 +394,11 @@ open class Carousel: View { // Add carousel items if views.count > 0 { var xPos = 0.0 - for x in 0...views.count - 1 { + for index in 0...views.count - 1 { // Add Carousel Slot let carouselSlot = View().with { $0.clipsToBounds = true - $0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1) } scrollView.addSubview(carouselSlot) scrollView.delegate = self @@ -412,35 +409,11 @@ open class Carousel: View { .pinLeading(xPos) .width(minimumSlotWidth) .height(slotHeight) - carouselSlot.layer.cornerRadius = 12.0 xPos = xPos + minimumSlotWidth + gutter.value - // Add received component - let item : CarouselSlotItemModel = .init(component: views[x]) - if let component = item.component { - carouselSlot.addSubview(component) - if slotAlignment != nil { - // If slotAlignment exist, use component's own sizes - component.widthAnchor.constraint(lessThanOrEqualTo: carouselSlot.widthAnchor).activate() - setSlotAlignment(contentView: component, parentView: carouselSlot) - } else { - // If no slotAlignment, should use full slot - component.pinToSuperView() - } - carouselSlot.backgroundColor = .clear - carouselSlot.layer.cornerRadius = 0 - if var surfacedView = component as? Surfaceable { - surfacedView.surface = surface - } - - // TO BE REMOVED AFTER VQA - borderWidth and borderColor below did set only for Testing purpose to VQA - carouselSlot.layer.borderWidth = 1.0 - carouselSlot.layer.borderColor = SurfaceColorConfiguration().with { - $0.lightColor = VDSColor.elementsPrimaryOnlight - $0.darkColor = VDSColor.elementsPrimaryOndark - }.getColor(self).cgColor - - } + let component = views[index] + carouselSlot.addSubview(component) + setSlotAlignment(contentView: component) } scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) } @@ -459,31 +432,38 @@ open class Carousel: View { } // Set slot alignment if provided. Used only when slot content have different heights or widths. - private func setSlotAlignment(contentView: UIView, parentView: View) { - parentView.backgroundColor = .clear + private func setSlotAlignment(contentView: UIView) { switch slotAlignment?.vertical { case .top: - contentView.topAnchor.constraint(equalTo: parentView.topAnchor).activate() - break + contentView + .pinTop() + .pinBottomLessThanOrEqualTo() case .middle: - contentView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor).activate() - break + contentView + .pinTopGreaterThanOrEqualTo() + .pinBottomLessThanOrEqualTo() + .pinCenterY() case .bottom: - contentView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).activate() - break + contentView + .pinTopGreaterThanOrEqualTo() + .pinBottom() default: break } switch slotAlignment?.horizontal { case .left: - contentView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor).activate() - break + contentView + .pinLeading() + .pinTrailingLessThanOrEqualTo() case .center: - contentView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor).activate() - break + contentView + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .pinCenterX() case .right: - parentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).activate() - break + contentView + .pinLeadingGreaterThanOrEqualTo() + .pinTrailing() default: break } } diff --git a/VDS/Components/Carousel/CarouselSlotItemModel.swift b/VDS/Components/Carousel/CarouselSlotItemModel.swift deleted file mode 100644 index 3cb7240a..00000000 --- a/VDS/Components/Carousel/CarouselSlotItemModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// CarouselSlotItemModel.swift -// VDS -// -// Created by Kanamarlapudi, Vasavi on 30/06/24. -// - -import Foundation -import UIKit -import VDSCoreTokens - -/// A custom data type that holds the style and component for a slot of the 'Carousel' component. -public struct CarouselSlotItemModel { - - public let defaultHeight: CGFloat = 50.0 - - /// Component to be show on Carousel slot - public var component: UIView? - - public init(component: UIView? = nil) { - self.component = component - } -} From f1b5fd18c962af74af62f63afd2e8e99894af552 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Fri, 19 Jul 2024 11:17:54 +0530 Subject: [PATCH 27/31] Digital ACT-191 ONEAPP-7013 story: removed unused code, updated Gutter pattern, renamed selectedIndex to groupIndex --- VDS/Components/Carousel/Carousel.swift | 48 ++++++++++++-------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 0a6e71f9..0cc6d738 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -39,7 +39,9 @@ open class Carousel: View { } /// Enum used to describe the peek for this component. - /// This is how much a tile is partially visible. It is measured by the distance between the edge of the tile and the edge of the viewport or carousel container. A peek can appear on the left and/or right edge of the carousel container or viewport, depending on the carousel’s scroll position. + /// This is how much a tile is partially visible. It is measured by the distance between the edge of + /// the tile and the edge of the viewport or carousel container. A peek can appear on the left and/or + /// right edge of the carousel container or viewport, depending on the carousel’s scroll position. public enum Peek: String, CaseIterable { case standard, minimum, none } @@ -53,23 +55,19 @@ open class Carousel: View { public enum Horizontal: String, CaseIterable { case left, center, right } - - /// Enum used to describe the width of a fixed value or percentage of parent's width. - public enum Width { - case percentage(CGFloat) - case value(CGFloat) - } - + /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. - public enum Gutter: String, CaseIterable { - case twelvePX = "12px" - case twentyFourPX = "24px" + public enum Gutter: String, CaseIterable , DefaultValuing { + case gutter3X = "3X" + case gutter6X = "6X" - var value: CGFloat { + public static var defaultValue: Self { UIDevice.isIPad ? .gutter6X : .gutter3X } + + public var value: CGFloat { switch self { - case .twelvePX: + case .gutter3X: VDSLayout.space3X - case .twentyFourPX: + case .gutter6X: VDSLayout.space6X } } @@ -82,9 +80,10 @@ open class Carousel: View { open var views: [UIView] = [] { didSet { setNeedsUpdate() } } /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. - open var gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX { didSet { setNeedsUpdate() } } + open var gutter: Gutter = Gutter.defaultValue { didSet { setNeedsUpdate() } } - /// The amount of slides visible in the carousel container at one time. The default value will be 3UP in tablet and 1UP in mobile. + /// The amount of slides visible in the carousel container at one time. + /// The default value will be 3UP in tablet and 1UP in mobile. open var layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP { didSet { carouselScrollBar.position = 0 @@ -116,11 +115,12 @@ open class Carousel: View { /// The default value will be 12px in tablet and 8px in mobile. These values are the default in order to avoid overlapping content within the carousel. open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } } - /// Options for user to configure the partially-visible tile in group. Setting peek to 'none' will display arrow navigation icons on mobile devices. + /// Options for user to configure the partially-visible tile in group. + /// Setting peek to 'none' will display arrow navigation icons on mobile devices. open var peek: Peek = .standard { didSet { setNeedsUpdate() } } /// The initial visible slide's index in the carousel. - open var selectedIndex: Int? { didSet { setNeedsUpdate() } } + open var groupIndex: Int? { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } } @@ -184,15 +184,13 @@ open class Carousel: View { private var containerViewHeightConstraint: NSLayoutConstraint? private var prevButtonLeadingConstraint: NSLayoutConstraint? private var nextButtonTrailingConstraint: NSLayoutConstraint? - private var containerLeadingConstraint: NSLayoutConstraint? // The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X - + var slotDefaultHeight = 50.0 var peekMinimum = 24.0 var minimumSlotWidth = 0.0 - var carouselScrollbarMinWidth = 96.0 //-------------------------------------------------- // MARK: - Lifecycle @@ -280,10 +278,10 @@ open class Carousel: View { updatePaginationControls() addCarouselSlots() - // If selectedIndex is received, the carousel should update its position. - if let selectedIndex { + // If groupIndex is received, the carousel should update its position. + if let groupIndex { let totalPos = totalPositions() - carouselScrollBar.position = selectedIndex >= totalPos ? totalPos : selectedIndex+1 + carouselScrollBar.position = groupIndex >= totalPos ? totalPos : groupIndex+1 } } @@ -295,7 +293,7 @@ open class Carousel: View { pagination = .init(kind: .lowContrast, floating: true) paginationDisplay = .none paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X - gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX + gutter = UIDevice.isIPad ? .gutter6X : .gutter3X peek = .standard } From 938364535b4a7dcde81cbd74a9343ec44102457a Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Fri, 19 Jul 2024 14:42:00 +0530 Subject: [PATCH 28/31] Digital ACT-191 ONEAPP-7013 story: removed redundant code which causes scrolling issue --- VDS/Components/Carousel/Carousel.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 0cc6d738..b7c0477a 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -304,12 +304,6 @@ open class Carousel: View { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } - /// Will be called when the scrubber position changes. - carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in - guard let self else { return } - updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange") - } - /// Will be called when the scrollbar thumb move forward. carouselScrollBar.onMoveForward = { [weak self] scrubberId in guard let self else { return } From 99fe7e22ac6462621e0bdaf4fd02f2689cca96f1 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Fri, 19 Jul 2024 16:41:40 +0530 Subject: [PATCH 29/31] Digital ACT-191 ONEAPP-7013 story: fixed observed issue on content scroll while changing peek, gutter and layout. --- VDS/Components/Carousel/Carousel.swift | 44 +++++++++----------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index b7c0477a..3a754ae8 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -120,7 +120,7 @@ open class Carousel: View { open var peek: Peek = .standard { didSet { setNeedsUpdate() } } /// The initial visible slide's index in the carousel. - open var groupIndex: Int? { didSet { setNeedsUpdate() } } + open var groupIndex: Int = 0 { didSet { setNeedsUpdate() } } /// If provided, will set the alignment for slot content when the slots has different heights. open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } } @@ -277,12 +277,6 @@ open class Carousel: View { updatePaginationControls() addCarouselSlots() - - // If groupIndex is received, the carousel should update its position. - if let groupIndex { - let totalPos = totalPositions() - carouselScrollBar.position = groupIndex >= totalPos ? totalPos : groupIndex+1 - } } /// Resets to default settings. @@ -304,6 +298,12 @@ open class Carousel: View { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } + /// Will be called when the scrubber position changes. + carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in + guard let self else { return } + updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange") + } + /// Will be called when the scrollbar thumb move forward. carouselScrollBar.onMoveForward = { [weak self] scrubberId in guard let self else { return } @@ -484,7 +484,7 @@ open class Carousel: View { break } } - minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value) + minimumSlotWidth = ceil(minimumSlotWidth / CGFloat(layout.value)) } private func nextButtonClick() { @@ -533,27 +533,18 @@ open class Carousel: View { let isScrollbarSuppressed = views.count > 0 && layout.value == views.count let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum if !isScrollbarSuppressed { - let subpart = minimumSlotWidth + gutter.value - let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(subpart)) - switch peek { - case .standard: - if UIDevice.isIPad { - xPos = xPosition - (minimumSlotWidth/(CGFloat(layout.value) + 3))/2 - } else if layout == .oneUP { - xPos = xPosition - gutter.value - (minimumSlotWidth/4)/2 - } - case .minimum: - xPos = isPeekMinimumOnTablet ? xPosition : xPosition - peekMinimum - case .none: - xPos = xPosition - } + let slotWidthWithGutter = minimumSlotWidth + gutter.value + let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(slotWidthWithGutter)) + let peekWidth = (containerView.frame.size.width - gutter.value - (Double(layout.value) * (minimumSlotWidth + gutter.value)))/2 + xPos = (peek == .none || isPeekMinimumOnTablet) ? xPosition : xPosition - gutter.value - peekWidth } } carouselScrollBar.scrubberId = position+1 let yPos = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) + scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: false) showPaginationControls() - onChangePublisher.send(position-1) + groupIndex = position-1 + onChangePublisher.send(groupIndex) } } @@ -571,9 +562,4 @@ extension Carousel: UIScrollViewDelegate { updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x) } - public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - var visibleRect = CGRect() - visibleRect.origin = scrollView.contentOffset - updateScrollbarPosition(targetContentOffsetXPos: visibleRect.origin.x) - } } From 7c431974ac02324bce273bb965b351a52e12497f Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Fri, 19 Jul 2024 18:04:50 +0530 Subject: [PATCH 30/31] minor changes --- VDS/Components/Carousel/Carousel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 3a754ae8..e05b94ea 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -56,7 +56,7 @@ open class Carousel: View { case left, center, right } - /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. + /// Space between each tile. The default value will be 6X in tablet and 3X in mobile. public enum Gutter: String, CaseIterable , DefaultValuing { case gutter3X = "3X" case gutter6X = "6X" @@ -79,7 +79,7 @@ open class Carousel: View { /// views used to render view in the carousel slots. open var views: [UIView] = [] { didSet { setNeedsUpdate() } } - /// Space between each tile. The default value will be 24px (6X) in tablet and 12px (3X) in mobile. + /// Space between each tile. The default value will be 6X in tablet and 3X in mobile. open var gutter: Gutter = Gutter.defaultValue { didSet { setNeedsUpdate() } } /// The amount of slides visible in the carousel container at one time. @@ -112,7 +112,7 @@ open class Carousel: View { open var paginationDisplay: PaginationDisplay = .none { didSet {setNeedsUpdate() } } /// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values. - /// The default value will be 12px in tablet and 8px in mobile. These values are the default in order to avoid overlapping content within the carousel. + /// The default value will be 3X in tablet and 2X in mobile. These values are the default in order to avoid overlapping content within the carousel. open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } } /// Options for user to configure the partially-visible tile in group. From afc4d71f38104afaf89985db11240830ded45f5a Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Fri, 19 Jul 2024 20:54:03 +0530 Subject: [PATCH 31/31] updated scroll animation to true --- VDS/Components/Carousel/Carousel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index e05b94ea..3e8aed2d 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -541,7 +541,7 @@ open class Carousel: View { } carouselScrollBar.scrubberId = position+1 let yPos = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: false) + scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) showPaginationControls() groupIndex = position-1 onChangePublisher.send(groupIndex)