vds_ios/VDS/Components/Pagination/Pagination.swift
2024-05-03 09:35:49 -05:00

247 lines
10 KiB
Swift

//
// Pagination.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 01/03/24.
//
import Foundation
import UIKit
import VDSTokens
import Combine
///Pagination is a control that enables customers to navigate multiple pages of content by selecting either a specific page or the next or previous set of four pages.
@objc(VDSPagination)
open class Pagination: View {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
///Maximum component width
private let maxWidth: CGFloat = 288.0
///Collectionview width anchor
private var collectionViewWidthAnchor: NSLayoutConstraint?
///Collectionview container Center X constraint
private var collectionContainerViewCenterXConstraint: NSLayoutConstraint?
///Selected page index
private var _selectedPageIndex: Int = 0
///Custom flow layout defined for the Pagination
private let flowLayout = PaginationFlowLayout()
///A root view for the pagination
public let containerView: View = View().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
///Collectionview to render pagination indexes
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.isScrollEnabled = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
collectionView.isAccessibilityElement = true
collectionView.register(PaginationCellItem.self, forCellWithReuseIdentifier: PaginationCellItem.identifier)
collectionView.backgroundColor = .clear
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
///Container view to hold collectionview to render pagination indexes and to handler accessibility.
private let collectionContainerView = PaginationContainer()
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
///Previous button to select previous page
public let previousButton: PaginationButton = .init(type: .previous)
///Next button to select next page
public let nextButton: PaginationButton = .init(type: .next)
/// A callback when the page changes. Passes parameters (selectedPage).
public var onPageDidSelect: ((Int) -> Void)?
/// Total number of pages, allows limit ranging from 0 to 9999.
@Clamping(range: 0...9999)
public var total: Int {
didSet {
previousButton.isHidden = true
nextButton.isHidden = total <= 1
_selectedPageIndex = 0
setNeedsUpdate()
updateSelection()
}
}
///Selected active page number and clips to total pages if selected index is greater than the total pages.
public var selectedPage: Int {
set {
if newValue >= total {
_selectedPageIndex = total - 1
} else if newValue < 0 {
_selectedPageIndex = 0
} else {
_selectedPageIndex = max(newValue - 1, 0)
}
setNeedsUpdate()
updateSelection()
}
get {
_selectedPageIndex + 1 //Returns selected page value not index
}
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
collectionContainerView.addSubview(collectionView)
containerView.addSubview(previousButton)
containerView.addSubview(collectionContainerView)
containerView.addSubview(nextButton)
addSubview(containerView)
containerView
.pinTop()
.pinBottom()
.pinLeadingGreaterThanOrEqualTo()
.pinTrailingLessThanOrEqualTo()
.pinCenterX()
.width(maxWidth)
.height(44)
previousButton
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailingGreaterThanOrEqualTo(collectionContainerView.leadingAnchor)
collectionContainerView
.pinTrailingGreaterThanOrEqualTo(nextButton.leadingAnchor)
.pinTop()
.pinBottom()
collectionView
.height(VDSLayout.space4X)
.pinCenterY()
.pinCenterX()
collectionViewWidthAnchor = collectionView.width(constant: 92)
nextButton
.pinTop()
.pinBottom()
.pinTrailing()
nextButton.onClick = onbuttonTapped
previousButton.onClick = onbuttonTapped
previousButton.isHidden = true
flowLayout.$collectionViewWidth
.receive(on: RunLoop.main)
.sink { [weak self] value in
self?.collectionViewWidthAnchor?.constant = value //As cell width is dynamic i.e cell may contain 2 or 3 or 4 charcters. Make sure that all the visible cells are displayed.
}.store(in: &subscribers)
collectionContainerView.onAccessibilityIncrement = { [weak self] in
guard let self else { return }
self.selectedPage = max(0, self.selectedPage + 1)
}
collectionContainerView.onAccessibilityDecrement = { [weak self] in
guard let self else { return }
self.selectedPage = max(0, self.selectedPage - 1)
}
}
///Updating the accessiblity values i.e elements, label, value other items for the component.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityElements = [previousButton, collectionContainerView, nextButton]
collectionContainerView.accessibilityLabel = "Pagination containing \(total) pages"
collectionContainerView.accessibilityValue = "Page \(selectedPage) of \(total) selected"
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
nextButton.surface = surface
previousButton.surface = surface
collectionView.reloadData()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
///When previous/next button is tapped
private func onbuttonTapped(_ sender: UIButton) {
let isNextAction = sender == nextButton
_selectedPageIndex = if isNextAction { _selectedPageIndex + 1 } else { _selectedPageIndex - 1 }
updateSelection()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self else { return }
UIAccessibility.post(notification: .announcement, argument: "Page \(self.selectedPage) of \(self.total) selected")
}
}
///Refreshing the UI based on the selected page
private func updateSelection() {
guard _selectedPageIndex < total else { return }
//Need to make selected page as second element so scrolling previous index of the selected page to left
collectionView.scrollToItem(at: IndexPath(row: max(_selectedPageIndex - 1, 0), section: 0), at: .left, animated: false)
previousButton.isHidden = _selectedPageIndex == 0
nextButton.isHidden = _selectedPageIndex == total - 1
collectionView.reloadData()
verifyIfMaxDigitChanged()
setNeedsUpdate()
}
///Identifying if there is any change in the digits of upcoming page
private func verifyIfMaxDigitChanged() {
let upperLimitPage = _selectedPageIndex + flowLayout.maxNumberOfColumns
let upperLimitDigits = upperLimitPage.digitCount //future value digits
switch (flowLayout.numberOfColumns, upperLimitDigits) {
case (_, 1), (_, 2):
flowLayout.numberOfColumns = 4
default:
flowLayout.numberOfColumns = 3
}
if upperLimitDigits != flowLayout.upperLimitDigits {
flowLayout.upperLimitDigits = upperLimitDigits
flowLayout.invalidateLayout()
collectionView.reloadData()
//Need to make selected page as second element so scrolling previous index of the selected page to left
collectionView.scrollToItem(at: IndexPath(row: max(_selectedPageIndex - 1, 0), section: 0), at: .left, animated: false)
}
}
}
extension Pagination: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
//--------------------------------------------------
// MARK: - UICollectionView Delegate & Datasource
//--------------------------------------------------
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { total }
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PaginationCellItem.identifier, for: indexPath) as? PaginationCellItem else { return UICollectionViewCell() }
cell.update(_selectedPageIndex, currentIndex: indexPath.row, surface: surface)
return cell
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard _selectedPageIndex != indexPath.row else { return }
_selectedPageIndex = indexPath.row
updateSelection()
onPageDidSelect?(selectedPage)
}
}
fileprivate extension Int {
//--------------------------------------------------
// MARK: - Extension on Int to identify number of digits in given number.
//--------------------------------------------------
var digitCount: Int {
numberOfDigits(in: self)
}
private func numberOfDigits(in number: Int) -> Int {
number < 10 && number >= 0 ? 1 : 1 + numberOfDigits(in: number/10)
}
}