From 1f2637ed0408846fa7bddf1664a74471de341648 Mon Sep 17 00:00:00 2001 From: Vasavi Kanamarlapudi Date: Tue, 27 Aug 2024 11:54:41 +0530 Subject: [PATCH] refactoring Carousel to use a UICollectionView --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Components/Carousel/Carousel.swift | 157 ++++++++---------- .../Carousel/CarouselSlotCell.swift | 90 ++++++++++ 3 files changed, 159 insertions(+), 92 deletions(-) create mode 100644 VDS/Components/Carousel/CarouselSlotCell.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index fc3417d2..10858c07 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 180636C82C29B0DF00C92D86 /* InputStepperLog.txt */; }; 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; + 183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */; }; 184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184023442C61E7AD00A412C8 /* PriceLockup.swift */; }; 184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */; }; 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; }; @@ -215,6 +216,7 @@ 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 = ""; }; + 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotCell.swift; sourceTree = ""; }; 184023442C61E7AD00A412C8 /* PriceLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceLockup.swift; sourceTree = ""; }; 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PriceLockupChangeLog.txt; sourceTree = ""; }; 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateViewCell.swift; sourceTree = ""; }; @@ -522,6 +524,7 @@ isa = PBXGroup; children = ( 18AE874F2C06FDA60075F181 /* Carousel.swift */, + 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */, 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */, 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */, 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */, @@ -1396,6 +1399,7 @@ EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */, EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */, EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */, + 183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */, 44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */, EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */, 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index 9e14d3d3..926df9f4 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -154,10 +154,21 @@ open class Carousel: View { $0.backgroundColor = .clear } - private var scrollView = UIScrollView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.backgroundColor = .clear - } + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout) + collectionView.isScrollEnabled = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.backgroundColor = .clear + collectionView.register(CarouselSlotCell.self, + forCellWithReuseIdentifier: CarouselSlotCell.identifier) + return collectionView + }() /// Previous button to show previous slide. private var previousButton = ButtonIcon().with { @@ -215,9 +226,9 @@ open class Carousel: View { containerView.addSubview(contentStackView) // Add scrollview - scrollContainerView.addSubview(scrollView) - scrollView.pinToSuperView() - + scrollContainerView.addSubview(collectionView) + collectionView.pinToSuperView() + // Add pagination button icons scrollContainerView.addSubview(previousButton) previousButton @@ -259,14 +270,24 @@ open class Carousel: View { /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() + updateScrollbar() + updateCarousel() + collectionView.reloadData() + } + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func updateScrollbar() { carouselScrollBar.numberOfSlides = views.count carouselScrollBar.layout = layout if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) { carouselScrollBar.position = 1 } carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false - + } + + private func updateCarousel() { // Mobile/Tablet layouts without peek - must show pagination controls. // If peek is ‘none’, pagination controls should show. So set to persistent. if peek == .none { @@ -284,12 +305,9 @@ open class Carousel: View { } updatePaginationControls() - addCarouselSlots() + updateContainerHeight() } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- + private func addlisteners() { nextButton.onClick = { _ in self.nextButtonClick() } previousButton.onClick = { _ in self.previousButtonClick() } @@ -365,47 +383,13 @@ open class Carousel: View { return height } - // Add carousel slots and load data if any - private func addCarouselSlots() { + // update carousel size and load data if any + private func updateContainerHeight() { getSlotWidth() 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() - } - - // Add carousel items - if views.count > 0 { - var xPos = 0.0 - for index in 0...views.count - 1 { - - // Add Carousel Slot - let carouselSlot = View().with { - $0.clipsToBounds = true - } - scrollView.addSubview(carouselSlot) - scrollView.delegate = self - - carouselSlot - .pinTop() - .pinBottom() - .pinLeading(xPos) - .width(minimumSlotWidth) - .height(slotHeight) - xPos = xPos + minimumSlotWidth + gutter.value - - let component = views[index] - carouselSlot.addSubview(component) - setSlotAlignment(contentView: component) - } - scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight) - } - let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height if carouselScrollBar.isHidden { containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight) @@ -419,43 +403,6 @@ open class Carousel: View { } } - // Set slot alignment if provided. Used only when slot content have different heights or widths. - private func setSlotAlignment(contentView: UIView) { - switch slotAlignment?.vertical { - case .top: - contentView - .pinTop() - .pinBottomLessThanOrEqualTo() - case .middle: - contentView - .pinTopGreaterThanOrEqualTo() - .pinBottomLessThanOrEqualTo() - .pinCenterY() - case .bottom: - contentView - .pinTopGreaterThanOrEqualTo() - .pinBottom() - default: break - } - - switch slotAlignment?.horizontal { - case .left: - contentView - .pinLeading() - .pinTrailingLessThanOrEqualTo() - case .center: - contentView - .pinLeadingGreaterThanOrEqualTo() - .pinTrailingLessThanOrEqualTo() - .pinCenterX() - case .right: - contentView - .pinLeadingGreaterThanOrEqualTo() - .pinTrailing() - default: break - } - } - // Get the slot width relative to the peak private func getSlotWidth() { let actualWidth = containerView.frame.size.width @@ -505,7 +452,7 @@ open class Carousel: View { } private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) { - let scrollContentSizeWidth = scrollView.contentSize.width + let scrollContentSizeWidth = collectionView.contentSize.width let totalPositions = totalPositions() let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions)))) let remindSpace = Int(targetContentOffsetXPos) % layoutSpace @@ -515,10 +462,11 @@ open class Carousel: View { updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved") } - // Update scrollview offset relative to scrollbar thumb position + // Update collectionview offset relative to scrollbar thumb position private func updateScrollPosition(position: Int, callbackText: String) { if carouselScrollBar.numberOfSlides > 0 { - let scrollContentSizeWidth = scrollView.contentSize.width + let scrollContentSizeWidth = collectionView.contentSize.width + let totalPositions = totalPositions() var xPos = 0.0 if position == 1 { @@ -536,8 +484,8 @@ open class Carousel: View { } } carouselScrollBar.scrubberId = position+1 - let yPos = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) + let yPos = collectionView.contentOffset.y + collectionView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true) showPaginationControls() groupIndex = position-1 onChangePublisher.send(groupIndex) @@ -557,5 +505,30 @@ extension Carousel: UIScrollViewDelegate { public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x) } - +} +extension Carousel: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + //-------------------------------------------------- + // MARK: - UICollectionView Delegate & Datasource + //-------------------------------------------------- + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + views.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CarouselSlotCell.identifier, for: indexPath) as? CarouselSlotCell else { return UICollectionViewCell() } + cell.contentView.subviews.forEach { $0.removeFromSuperview() } + let component = views[indexPath.row] + cell.update(with: component, slotAlignment: slotAlignment, surface: surface) + cell.layoutIfNeeded() + return cell + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return gutter.value + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: minimumSlotWidth, height: fetchCarouselHeight()) + } + } diff --git a/VDS/Components/Carousel/CarouselSlotCell.swift b/VDS/Components/Carousel/CarouselSlotCell.swift new file mode 100644 index 00000000..11807747 --- /dev/null +++ b/VDS/Components/Carousel/CarouselSlotCell.swift @@ -0,0 +1,90 @@ +// +// CarouselSlotCell.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 23/08/24. +// + +import Foundation +import UIKit + +final class CarouselSlotCell: UICollectionViewCell { + + ///Identifier for the Calendar Date Cell. + static let identifier: String = String(describing: CarouselSlotCell.self) + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setUp() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var surface: Surface = .light + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + + /// Configuring the cell with default setup. + private func setUp() { + isAccessibilityElement = true + } + + /// Updating UI based on data along with surface. + func update(with component: UIView, slotAlignment: Carousel.CarouselSlotAlignmentModel?, surface: Surface) { + self.surface = surface + contentView.addSubview(component) + if var surfacedView = component as? Surfaceable { + surfacedView.surface = surface + } + setSlotAlignment(alignment: slotAlignment, contentView: component) + } + + // Set slot alignment if provided. Used only when slot content have different heights or widths. + private func setSlotAlignment(alignment: Carousel.CarouselSlotAlignmentModel?, contentView: UIView) { + switch alignment?.vertical { + case .top: + contentView + .pinTop() + .pinBottomLessThanOrEqualTo() + case .middle: + contentView + .pinTopGreaterThanOrEqualTo() + .pinBottomLessThanOrEqualTo() + .pinCenterY() + case .bottom: + contentView + .pinTopGreaterThanOrEqualTo() + .pinBottom() + default: break + } + + switch alignment?.horizontal { + case .left: + contentView + .pinLeading() + .pinTrailingLessThanOrEqualTo() + case .center: + contentView + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .pinCenterX() + case .right: + contentView + .pinLeadingGreaterThanOrEqualTo() + .pinTrailing() + default: break + } + } + +}