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