Digital ACT-191 ONEAPP-7013 story: slot alignment and rendering data

This commit is contained in:
Vasavi Kanamarlapudi 2024-07-04 20:24:33 +05:30
parent 8619c64109
commit 77288e4c54
5 changed files with 212 additions and 33 deletions

View File

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */; };
18013CEF2C355C5200907F18 /* CarouselRenderItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */; };
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; };
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; };
1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; };
@ -19,11 +21,9 @@
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 */; };
18AE87542C06FE610075F181 /* CarouselChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18AE87532C06FE610075F181 /* CarouselChangeLog.txt */; };
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 */; };
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; };
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 */; };
@ -209,6 +209,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotItemModel.swift; sourceTree = "<group>"; };
18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselRenderItemStyle.swift; sourceTree = "<group>"; };
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = "<group>"; };
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = "<group>"; };
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = "<group>"; };
@ -502,6 +504,8 @@
18AE874F2C06FDA60075F181 /* Carousel.swift */,
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */,
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */,
18013CEC2C355BF900907F18 /* CarouselSlotItemModel.swift */,
18013CEE2C355C5200907F18 /* CarouselRenderItemStyle.swift */,
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */,
);
path = Carousel;
@ -1363,6 +1367,7 @@
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */,
EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */,
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */,
18013CEF2C355C5200907F18 /* CarouselRenderItemStyle.swift in Sources */,
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */,
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */,
EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */,
@ -1405,6 +1410,7 @@
EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */,
EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */,
EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */,
18013CED2C355BF900907F18 /* CarouselSlotItemModel.swift in Sources */,
18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */,
EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */,
5FC35BE328D51405004EBEAC /* Button.swift in Sources */,

View File

@ -33,21 +33,6 @@ open class Carousel: View {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
/// 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 persistent, none
@ -75,6 +60,21 @@ open class Carousel: View {
case value(CGFloat)
}
/// 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
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
@ -123,11 +123,12 @@ open class Carousel: View {
get { return _layout }
set {
_layout = newValue
carouselScrollBar.position = 0
setNeedsUpdate()
}
}
/// A callback when moving the carousel. Returns event object and selectedGroupIndex.
/// A callback when moving the carousel. Returns initial visible slide's index in the carousel.
open var onChange: ((Int) -> Void)? {
get { nil }
set {
@ -182,7 +183,42 @@ open class Carousel: View {
open var selectedIndex: Int? { didSet { setNeedsUpdate() } }
/// 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? {
get { return _slotAlignment }
set {
if let newValue {
_slotAlignment = newValue
} else {
_slotAlignment = nil
}
setNeedsUpdate()
}
}
/// Render item style. If provided, the slot gets the background, width, height, border-radius.
open var renderItemStyle: CarouselRenderItemStyle? {
get { return _renderItemStyle }
set {
if let newValue {
_renderItemStyle = newValue
} else {
_renderItemStyle = nil
}
setNeedsUpdate()
}
}
/// Render item. It passes a data array object and expects the styled component to apply in return.
open var renderItem: CarouselSlotItemModel? {
get { _renderItem }
set {
if let newValue {
_renderItem = newValue
} else {
_renderItem = nil
}
}
}
//--------------------------------------------------
// MARK: - Private Properties
@ -250,9 +286,12 @@ open class Carousel: View {
internal var _gutter: Gutter = UIDevice.isIPad ? .twentyFourPX : .twelvePX
internal var _peek: Peek = .standard
internal var _numberOfSlides: Int = 1
internal var _slotAlignment: CarouselSlotAlignmentModel? = nil
internal var _renderItemStyle: CarouselRenderItemStyle? = nil
internal var _renderItem: CarouselSlotItemModel? = nil
private var _width: Width? = nil
private var selectedGroupIndex: Int? { didSet { setNeedsUpdate() } }
private var selectedGroupIndex: Int? = nil
private var containerStackHeightConstraint: NSLayoutConstraint?
private var containerViewHeightConstraint: NSLayoutConstraint?
private var prevButtonLeadingConstraint: NSLayoutConstraint?
@ -323,6 +362,7 @@ open class Carousel: View {
.heightGreaterThanEqualTo(containerSize.height)
addlisteners()
updatePaginationInset()
}
/// Used to make changes to the View based off a change events or from local properties.
@ -348,7 +388,9 @@ open class Carousel: View {
carouselScrollBar.numberOfSlides = data.count
carouselScrollBar.layout = _layout
carouselScrollBar.position = (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) ? 1 : carouselScrollBar.position
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.
@ -392,31 +434,32 @@ open class Carousel: View {
nextButton.onClick = { _ in self.nextButtonClick() }
previousButton.onClick = { _ in self.previousButtonClick() }
/// Will be called when the thumb move forward.
/// 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 thumb move backward.
/// 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 thumb touch start.
/// 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 thumb touch end.
/// 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()
@ -428,6 +471,7 @@ open class Carousel: View {
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
@ -438,6 +482,7 @@ open class Carousel: View {
}
}
// Add carousel slots and load data if any
private func addCarouselSlots() {
getSlotWidth()
if containerView.frame.size.width > 0 {
@ -454,6 +499,8 @@ open class Carousel: View {
if data.count > 0 {
var xPos = 0.0
for x in 0...data.count - 1 {
// Add Carousel Slot
let carouselSlot = View().with {
$0.clipsToBounds = true
$0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1)
@ -470,6 +517,29 @@ open class Carousel: View {
.height(slotHeight)
carouselSlot.layer.cornerRadius = 12.0
xPos = xPos + minimumSlotWidth + gutter.value
// Add subview for content to Carousel Slot
let contentView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = UIColor(red: CGFloat(216) / 255.0, green: CGFloat(218) / 255.0, blue: CGFloat(218) / 255.0, alpha: 1)
}
carouselSlot.addSubview(contentView)
// Add received component
let item : CarouselSlotItemModel = .init(style: renderItemStyle, component: data[x] as? UIView)
if let component = item.component {
if slotAlignment != nil {
setSlotAlignment(contentView: contentView, parentView: carouselSlot)
} else {
contentView.pinToSuperView()
}
contentView.addSubview(component)
component.pinToSuperView()
contentView.layer.cornerRadius = component.layer.cornerRadius
if var surfacedView = component as? Surfaceable {
surfacedView.surface = surface
}
}
}
scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight)
}
@ -487,6 +557,37 @@ open class Carousel: View {
}
}
// Set slot alignment if provided. Used only when slot content have different heights or widths.
private func setSlotAlignment(contentView: View, parentView: View) {
parentView.backgroundColor = .clear
switch slotAlignment?.vertical {
case .top:
contentView.topAnchor.constraint(equalTo: parentView.topAnchor).activate()
break
case .middle:
contentView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor).activate()
break
case .bottom:
contentView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).activate()
break
default: break
}
switch slotAlignment?.horizontal {
case .left:
contentView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor).activate()
break
case .center:
contentView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor).activate()
break
case .right:
parentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).activate()
break
default: break
}
}
// Get the slot width relative to the peak
private func getSlotWidth() {
let actualWidth = containerView.frame.size.width
let isScrollbarSuppressed = data.count > 0 && layout.value == data.count
@ -505,7 +606,7 @@ open class Carousel: View {
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
minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum - gutter.value
case .none:
break
}
@ -525,6 +626,7 @@ open class Carousel: View {
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
// The size of slot depends on the selected aspect ratio
private func ratioSize(for width: CGFloat) -> CGSize {
var height: CGFloat = width
@ -575,6 +677,7 @@ open class Carousel: View {
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
@ -598,7 +701,7 @@ open class Carousel: View {
xPos = xPosition - gutter.value - (minimumSlotWidth/4)/2
}
case .minimum:
xPos = isPeekMinimumOnTablet ? xPosition : xPosition - peekMinimum/2
xPos = isPeekMinimumOnTablet ? xPosition : xPosition - peekMinimum
case .none:
xPos = xPosition
}
@ -608,9 +711,13 @@ open class Carousel: View {
let yPos = scrollView.contentOffset.y
scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true)
showPaginationControls()
selectedIndex = ((position-1) * layout.value) + 1
onChangePublisher.send(selectedIndex ?? 1)
selectedGroupIndex = position
}
}
// 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)))
}

View File

@ -0,0 +1,33 @@
//
// CarouselRenderItemStyle.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 30/06/24.
//
import Foundation
import UIKit
import VDSCoreTokens
/// A custom data type that holds the style props if provided any.
public struct CarouselRenderItemStyle {
/// BackgroundColor for slot
public let backgroundColor: String?
/// Height for slot
public var height: CGFloat?
/// BorderRadius for slot
public var borderRadius: CGFloat?
/// Width for slot
public var width: CGFloat?
public init(backgroundColor: String?, height: CGFloat?, width: CGFloat?, borderRadius: CGFloat?) {
self.backgroundColor = backgroundColor
self.height = height
self.borderRadius = borderRadius ?? 12.0
self.width = width
}
}

View File

@ -7,17 +7,19 @@
import Foundation
/// Custom data type for slotAlignment prop for 'Carousel' component.
/// 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 {
/// Text that shown to an indicator for legend
public var vertical: String
/// Used for vertical alignment of slot alignment.
public var vertical: Carousel.Vertical
/// Date to an indicator
public var horizontal: String
/// Used for horizontal alignment of slot alignment.
public var horizontal: Carousel.Horizontal
public init(vertical: String, horizontal: String) {
public init(vertical: Carousel.Vertical, horizontal: Carousel.Horizontal) {
self.vertical = vertical
self.horizontal = horizontal
}

View File

@ -0,0 +1,31 @@
//
// CarouselSlotItemModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 30/06/24.
//
import Foundation
import UIKit
import VDSCoreTokens
/// A custom data type that holds the style and component for a slot of the 'Carousel' component.
public struct CarouselSlotItemModel {
/// Style props if provided any
public var style: CarouselRenderItemStyle?
/// Component to be show on Carousel slot
public var component: UIView?
public init(style: CarouselRenderItemStyle? = nil, component: UIView? = nil) {
self.style = style
self.component = component
if let color = style?.backgroundColor {
self.component?.backgroundColor = .init(hexString: color)
}
if let borderRadius = style?.borderRadius {
self.component?.layer.cornerRadius = borderRadius
}
}
}