Merge branch 'vasavk/carousel' into 'develop'

VDS Brand 3.0 Base Carousel for IOS

See merge request BPHV_MIPS/vds_ios!268
This commit is contained in:
Bruce, Matt R 2024-07-19 17:07:48 +00:00
commit fb629a192b
7 changed files with 663 additions and 7 deletions

View File

@ -18,7 +18,10 @@
18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; };
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; };
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; };
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; };
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; };
18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; };
18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; };
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
@ -219,7 +222,11 @@
18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = "<group>"; };
18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = "<group>"; };
18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = "<group>"; };
18AE874F2C06FDA60075F181 /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = "<group>"; };
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = "<group>"; };
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotAlignmentModel.swift; sourceTree = "<group>"; };
18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = "<group>"; };
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPaginationModel.swift; sourceTree = "<group>"; };
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = "<group>"; };
18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
@ -487,6 +494,17 @@
path = Breadcrumbs;
sourceTree = "<group>";
};
18AE874E2C06FD610075F181 /* Carousel */ = {
isa = PBXGroup;
children = (
18AE874F2C06FDA60075F181 /* Carousel.swift */,
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */,
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */,
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */,
);
path = Carousel;
sourceTree = "<group>";
};
440B84C82BD8E0CE004A732A /* Table */ = {
isa = PBXGroup;
children = (
@ -657,6 +675,7 @@
18A65A002B96E7E1006602CC /* Breadcrumbs */,
EA0FC2BE2912D18200DF80B4 /* Buttons */,
18A3F1202BD8F5DE00498E4A /* Calendar */,
18AE874E2C06FD610075F181 /* Carousel */,
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
EAF7F092289985E200B287F5 /* Checkbox */,
EAC58C1F2BF127F000BA39FA /* DatePicker */,
@ -1290,6 +1309,7 @@
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */,
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */,
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
44BD43B62C04866600644F87 /* TableRowModel.swift in Sources */,
@ -1301,6 +1321,7 @@
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */,
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
@ -1339,6 +1360,7 @@
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */,
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */,
EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */,
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */,
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */,
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */,
EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */,

View File

@ -0,0 +1,565 @@
//
// Carousel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 29/05/24.
//
import Foundation
import UIKit
import VDSCoreTokens
import Combine
/// A carousel is a collection of related content in a row that a customer can navigate through horizontally.
/// Use this component to show content that is supplementary, not essential for task completion.
@objc(VDSCarousel)
open class Carousel: View {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero)
}
public override init(frame: CGRect) {
super.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
/// Enum used to describe the pagination display for this component.
public enum PaginationDisplay: String, CaseIterable {
case persistent, none
}
/// 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 carousels scroll position.
public enum Peek: String, CaseIterable {
case standard, minimum, none
}
/// 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
}
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
public enum Gutter: String, CaseIterable , DefaultValuing {
case gutter3X = "3X"
case gutter6X = "6X"
public static var defaultValue: Self { UIDevice.isIPad ? .gutter6X : .gutter3X }
public var value: CGFloat {
switch self {
case .gutter3X:
VDSLayout.space3X
case .gutter6X:
VDSLayout.space6X
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// views used to render view in the carousel slots.
open var views: [UIView] = [] { didSet { setNeedsUpdate() } }
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
open var gutter: Gutter = Gutter.defaultValue { didSet { 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 = UIDevice.isIPad ? .threeUP : .oneUP {
didSet {
carouselScrollBar.position = 0
setNeedsUpdate()
}
}
/// A callback when moving the carousel. Returns selectedGroupIndex.
open var onChange: ((Int) -> Void)? {
get { nil }
set {
onChangeCancellable?.cancel()
if let newValue {
onChangeCancellable = onChangePublisher
.sink { c in
newValue(c)
}
}
}
}
/// Config object for pagination.
open var pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) { didSet {setNeedsUpdate() } }
/// If provided, will determine the conditions to render the pagination arrows.
open var paginationDisplay: PaginationDisplay = .none { didSet {setNeedsUpdate() } }
/// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values.
/// The default value will be 3X in tablet and 2X in mobile. These values are the default in order to avoid overlapping content within the carousel.
open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } }
/// Options for user to configure the partially-visible tile in group.
/// Setting peek to 'none' will display arrow navigation icons on mobile devices.
open var peek: Peek = .standard { didSet { setNeedsUpdate() } }
/// The initial visible slide's index in the carousel.
open var groupIndex: Int = 0 { didSet { setNeedsUpdate() } }
/// If provided, will set the alignment for slot content when the slots has different heights.
open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal var containerSize: CGSize { CGSize(width: frame.size.width, height: 44) }
private let contentStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
$0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
$0.backgroundColor = .clear
}
internal var carouselScrollBar = CarouselScrollbar().with {
$0.layout = UIDevice.isIPad ? .threeUP : .oneUP
$0.position = 0
$0.backgroundColor = .clear
}
internal var containerView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = .clear
}
internal var scrollContainerView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = .clear
}
private var scrollView = UIScrollView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$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
}
/// A publisher for when moving the carousel. Passes parameters selectedGroupIndex (position).
open var onChangePublisher = PassthroughSubject<Int, Never>()
private var onChangeCancellable: AnyCancellable?
private var containerStackHeightConstraint: NSLayoutConstraint?
private var containerViewHeightConstraint: NSLayoutConstraint?
private var prevButtonLeadingConstraint: NSLayoutConstraint?
private var nextButtonTrailingConstraint: 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
var slotDefaultHeight = 50.0
var peekMinimum = 24.0
var minimumSlotWidth = 0.0
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
}
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
isAccessibilityElement = false
// Add containerView
addSubview(containerView)
containerView
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailing()
.heightGreaterThanEqualTo(containerSize.height)
containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate()
// Add content stackview
containerView.addSubview(contentStackView)
// Add scrollview
scrollContainerView.addSubview(scrollView)
scrollView.pinToSuperView()
// Add pagination button icons
scrollContainerView.addSubview(previousButton)
previousButton
.pinLeadingGreaterThanOrEqualTo()
.pinCenterY()
scrollContainerView.addSubview(nextButton)
nextButton
.pinTrailingLessThanOrEqualTo()
.pinCenterY()
// Add scroll container view & carousel scrollbar
contentStackView.addArrangedSubview(scrollContainerView)
contentStackView.addArrangedSubview(carouselScrollBar)
contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
contentStackView
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailing()
.heightGreaterThanEqualTo(containerSize.height)
addlisteners()
updatePaginationInset()
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
carouselScrollBar.numberOfSlides = views.count
carouselScrollBar.layout = layout
if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) {
carouselScrollBar.position = 1
}
carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false
// Mobile/Tablet layouts without peek - must show pagination controls.
// If peek is none, pagination controls should show. So set to persistent.
if peek == .none {
paginationDisplay = .persistent
}
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
if UIDevice.isIPad && peek == .minimum {
peek = .standard
}
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP {
peek = .minimum
}
updatePaginationControls()
addCarouselSlots()
}
/// Resets to default settings.
open override func reset() {
super.reset()
shouldUpdateView = false
layout = UIDevice.isIPad ? .threeUP : .oneUP
pagination = .init(kind: .lowContrast, floating: true)
paginationDisplay = .none
paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
gutter = UIDevice.isIPad ? .gutter6X : .gutter3X
peek = .standard
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func addlisteners() {
nextButton.onClick = { _ in self.nextButtonClick() }
previousButton.onClick = { _ in self.previousButtonClick() }
/// Will be called when the scrubber position changes.
carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange")
}
/// Will be called when the scrollbar thumb move forward.
carouselScrollBar.onMoveForward = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onMoveForward")
}
/// Will be called when the scrollbar thumb move backward.
carouselScrollBar.onMoveBackward = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward")
}
/// Will be called when the scrollbar thumb touch start.
carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart")
}
/// Will be called when the scrollbar thumb touch end.
carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd")
}
}
// Update pagination buttons with selected surface, kind, floating values
private func updatePaginationControls() {
containerView.surface = surface
showPaginationControls()
previousButton.kind = pagination.kind
previousButton.floating = pagination.floating
nextButton.kind = pagination.kind
nextButton.floating = pagination.floating
previousButton.surface = surface
nextButton.surface = surface
}
// Show/Hide pagination buttons of Carousel based on First or Middle or Last
private 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)
}
}
private func estimateHeightFor(component: UIView, with itemWidth: CGFloat) -> CGFloat {
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
let estItemSize = component.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return estItemSize.height
}
private func fetchCarouselHeight() -> CGFloat {
var height = slotDefaultHeight
if views.count > 0 {
for index in 0...views.count - 1 {
let estHeight = estimateHeightFor(component: views[index], with: minimumSlotWidth)
height = estHeight > height ? estHeight : height
}
}
return height
}
// Add carousel slots and load data if any
private func addCarouselSlots() {
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)
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
}
}
// 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
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
let isPeekNone: Bool = peek == .none
minimumSlotWidth = isScrollbarSuppressed || isPeekMinimumOnTablet || isPeekNone ? actualWidth - ((CGFloat(layout.value)-1) * gutter.value): actualWidth - (CGFloat(layout.value) * gutter.value)
if !isScrollbarSuppressed {
switch peek {
case .standard:
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
if UIDevice.isIPad {
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 3))
} else if layout == .oneUP {
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4)
}
case .minimum:
// Peek Mimumum Width: 24px from edge of container (at the default view of the carousel with one peek visible)
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum - gutter.value
case .none:
break
}
}
minimumSlotWidth = ceil(minimumSlotWidth / CGFloat(layout.value))
}
private func nextButtonClick() {
carouselScrollBar.position = carouselScrollBar.position+1
showPaginationControls()
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
private func previousButtonClick() {
carouselScrollBar.position = carouselScrollBar.position-1
showPaginationControls()
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
private func updatePaginationInset() {
prevButtonLeadingConstraint?.isActive = false
nextButtonTrailingConstraint?.isActive = false
prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset)
nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset)
prevButtonLeadingConstraint?.isActive = true
nextButtonTrailingConstraint?.isActive = true
}
private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) {
let scrollContentSizeWidth = scrollView.contentSize.width
let totalPositions = totalPositions()
let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions))))
let remindSpace = Int(targetContentOffsetXPos) % layoutSpace
var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1
contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos
carouselScrollBar.position = contentPos
updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved")
}
// Update scrollview offset relative to scrollbar thumb position
private func updateScrollPosition(position: Int, callbackText: String) {
if carouselScrollBar.numberOfSlides > 0 {
let scrollContentSizeWidth = scrollView.contentSize.width
let totalPositions = totalPositions()
var xPos = 0.0
if position == 1 {
xPos = 0.0
} else if position == totalPositions {
xPos = scrollContentSizeWidth - containerView.frame.size.width
} else {
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
if !isScrollbarSuppressed {
let slotWidthWithGutter = minimumSlotWidth + gutter.value
let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(slotWidthWithGutter))
let peekWidth = (containerView.frame.size.width - gutter.value - (Double(layout.value) * (minimumSlotWidth + gutter.value)))/2
xPos = (peek == .none || isPeekMinimumOnTablet) ? xPosition : xPosition - gutter.value - peekWidth
}
}
carouselScrollBar.scrubberId = position+1
let yPos = scrollView.contentOffset.y
scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true)
showPaginationControls()
groupIndex = position-1
onChangePublisher.send(groupIndex)
}
}
// Get the overall positions of the carousel scrollbar relative to the slides and selected layout
private func totalPositions() -> Int {
return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(layout.value)))
}
}
extension Carousel: UIScrollViewDelegate {
//--------------------------------------------------
// MARK: - UIScrollView Delegate
//--------------------------------------------------
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x)
}
}

View File

@ -0,0 +1,15 @@
MM/DD/YYYY
----------------
06/22/2023
----------------
- Initial Beta Release
10/02/2023
----------------
- Removed (Beta) from header. Removed deprecated sections and “New” badge from Kind section.
11/20/2023
----------------
- Updated visuals to reflect new corner radius value - 12px
- Updated focus border corner radius to 14px

View File

@ -0,0 +1,26 @@
//
// CarouselPaginationModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 06/06/24.
//
import Foundation
import UIKit
/// Custom data type for pagination prop for 'Carousel' component.
extension Carousel {
public struct CarouselPaginationModel {
/// Pagination supports Button icon property 'kind'.
public var kind: ButtonIcon.Kind
/// Pagination supports Button icon property 'floating'.
public var floating: Bool
public init(kind: ButtonIcon.Kind, floating: Bool) {
self.kind = kind
self.floating = floating
}
}
}

View File

@ -0,0 +1,27 @@
//
// CarouselSlotAlignmentModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 31/05/24.
//
import Foundation
/// Custom data type for the SlotAlignment prop for the 'carousel' component.
extension Carousel {
/// Used only when slot content have different heights or widths.
public struct CarouselSlotAlignmentModel {
/// Used for vertical alignment of slot alignment.
public var vertical: Carousel.Vertical
/// Used for horizontal alignment of slot alignment.
public var horizontal: Carousel.Horizontal
public init(vertical: Carousel.Vertical, horizontal: Carousel.Horizontal) {
self.vertical = vertical
self.horizontal = horizontal
}
}
}

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

View File

@ -26,6 +26,7 @@ Using the system allows designers and developers to collaborate more easily and
- ``ButtonIcon``
- ``ButtonGroup``
- ``CalendarBase``
- ``Carousel``
- ``CarouselScrollbar``
- ``Checkbox``
- ``CheckboxItem``