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