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 + } + } +}