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) {