Digital ACT-191 ONEAPP-7013 story: show slides on Scrollview

This commit is contained in:
vasavk 2024-06-11 15:16:52 +05:30
parent 0fc8c5c530
commit 35e4cf0301
3 changed files with 186 additions and 108 deletions

View File

@ -30,6 +30,92 @@ open class Carousel: View {
super.init(coder: coder) 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 // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
@ -38,8 +124,30 @@ open class Carousel: View {
/// Data used to render tilelets in the carousel. /// Data used to render tilelets in the carousel.
open var data: [Any] = [] { didSet { setNeedsUpdate() } } 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 { open var gutter: Gutter {
get { return _gutter } get { return _gutter }
set { set {
@ -113,87 +221,7 @@ open class Carousel: View {
/// If provided, will set the alignment for slot content when the slots has different heights. /// If provided, will set the alignment for slot content when the slots has different heights.
open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } } 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 // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
@ -204,7 +232,7 @@ open class Carousel: View {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical $0.axis = .vertical
$0.distribution = .fill $0.distribution = .fill
$0.spacing = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X $0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
$0.backgroundColor = .clear $0.backgroundColor = .clear
} }
@ -254,19 +282,21 @@ open class Carousel: View {
internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP internal var _layout: Layout = UIDevice.isIPad ? .threeUP : .oneUP
internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) internal var _pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true)
internal var _paginationDisplay: PaginationDisplay = .none 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 _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX
internal var _peek: Peek = .none internal var _peek: Peek = .none
internal var _numberOfSlides: Int = 1 internal var _numberOfSlides: Int = 1
private var _width: Width? = nil
private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } } private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } }
private var containerStackHeightConstraint: NSLayoutConstraint? private var containerStackHeightConstraint: NSLayoutConstraint?
private var containerViewHeightConstraint: NSLayoutConstraint? private var containerViewHeightConstraint: NSLayoutConstraint?
// The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile. // 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 // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
@ -293,6 +323,7 @@ open class Carousel: View {
// add scrollview // add scrollview
scrollContainerView.addSubview(scrollView) scrollContainerView.addSubview(scrollView)
scrollView.pinToSuperView()
// add pagination button icons // add pagination button icons
scrollContainerView.addSubview(previousButton) scrollContainerView.addSubview(previousButton)
@ -308,37 +339,84 @@ open class Carousel: View {
// add scroll container view & carousel scrollbar // add scroll container view & carousel scrollbar
contentStackView.addArrangedSubview(scrollContainerView) contentStackView.addArrangedSubview(scrollContainerView)
contentStackView.addArrangedSubview(carouselScrollBar) contentStackView.addArrangedSubview(carouselScrollBar)
contentStackView.setCustomSpacing(space, after: scrollContainerView) contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
contentStackView contentStackView
.pinTop() .pinTop()
.pinBottom() .pinBottom()
.pinLeading() .pinLeading()
.pinTrailing() .pinTrailing()
.heightGreaterThanEqualTo(space+containerSize.height) .heightGreaterThanEqualTo(scrollbarTopSpace + containerSize.height)
contentStackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() contentStackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate()
} }
open override func updateView() { open override func updateView() {
containerViewHeightConstraint?.isActive = false
super.updateView() super.updateView()
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 200) containerViewHeightConstraint?.isActive = false
containerViewHeightConstraint?.isActive = true 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) previousButton.isHidden = (paginationDisplay == .none)
nextButton.isHidden = (paginationDisplay == .none) nextButton.isHidden = (paginationDisplay == .none)
previousButton.kind = pagination.kind previousButton.kind = pagination.kind
previousButton.floating = pagination.floating previousButton.floating = pagination.floating
nextButton.kind = pagination.kind nextButton.kind = pagination.kind
nextButton.floating = pagination.floating nextButton.floating = pagination.floating
layoutIfNeeded() previousButton.surface = surface
nextButton.surface = surface
} }
open override func reset() { func getSlotWidth() {
// for subview in subviews { let actualWidth = containerView.frame.size.width
// for recognizer in subview.gestureRecognizers ?? [] { minimumSlotWidth = actualWidth - (CGFloat(layout.value) * gutter.value)
// subview.removeGestureRecognizer(recognizer) switch peek {
// } case .standard:
// } minimumSlotWidth = minimumSlotWidth - (3*peekMinimum)
super.reset() case .minimum:
minimumSlotWidth = minimumSlotWidth - peekMinimum
case .none:
break
}
minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value)
} }
} }

View File

@ -12,10 +12,10 @@ import UIKit
extension Carousel { extension Carousel {
public struct CarouselPaginationModel { public struct CarouselPaginationModel {
/// Button icon property 'kind' is supported /// Pagination supports Button icon property 'kind'.
public var kind: ButtonIcon.Kind public var kind: ButtonIcon.Kind
/// Button icon property 'floating' is supported /// Pagination supports Button icon property 'floating'.
public var floating: Bool public var floating: Bool
public init(kind: ButtonIcon.Kind, floating: Bool) { public init(kind: ButtonIcon.Kind, floating: Bool) {

View File

@ -45,13 +45,13 @@ open class CarouselScrollbar: View {
} }
/// The number of slides that can appear at once in a set in a carousel container. /// The number of slides that can appear at once in a set in a carousel container.
open var selectedLayout: Layout? { open var layout: Layout? {
get { return _selectedLayout } get { return _layout }
set { set {
if let newValue { if let newValue {
_selectedLayout = newValue _layout = newValue
} else { } else {
_selectedLayout = .oneUP _layout = .oneUP
} }
setThumbWidth() setThumbWidth()
scrollThumbToPosition(position) scrollThumbToPosition(position)
@ -198,7 +198,7 @@ open class CarouselScrollbar: View {
//-------------------------------------------------- //--------------------------------------------------
// Sizes are from InVision design specs. // Sizes are from InVision design specs.
internal var containerSize: CGSize { CGSize(width: 45, height: 44) } 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 _numberOfSlides: Int = 1
internal var totalPositions: Int = 1 internal var totalPositions: Int = 1
internal var _position: 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 // Compute track width and should maintain minimum thumb width if needed
private func setThumbWidth() { 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 computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth) thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
thumbView.frame.size.width = CGFloat(thumbWidth) thumbView.frame.size.width = CGFloat(thumbWidth)
@ -362,7 +362,7 @@ open class CarouselScrollbar: View {
} }
private func checkPositions() { private func checkPositions() {
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value))) totalPositions = Int (ceil (Double(numberOfSlides) / Double(_layout.value)))
} }
private func scrollThumbToPosition(_ position: Int) { private func scrollThumbToPosition(_ position: Int) {