264 lines
10 KiB
Swift
264 lines
10 KiB
Swift
//
|
|
// Pagination.swift
|
|
// VDS
|
|
//
|
|
// Created by Bandaru, Krishna Kishore on 01/03/24.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import VDSCoreTokens
|
|
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.
|
|
@objcMembers
|
|
@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).
|
|
open 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.
|
|
open 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
|
|
}
|
|
}
|
|
|
|
private var paginationDescription: String {
|
|
"Page \(selectedPage) of \(total) selected"
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Overrides
|
|
//--------------------------------------------------
|
|
/// Executed on initialization for this View.
|
|
open override func setup() {
|
|
super.setup()
|
|
|
|
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)
|
|
}
|
|
|
|
collectionContainerView.bridge_accessibilityLabelBlock = { [weak self] in
|
|
guard let self else { return "" }
|
|
return "Pagination containing \(total) pages"
|
|
}
|
|
|
|
collectionContainerView.bridge_accessibilityValueBlock = { [weak self] in
|
|
guard let self else { return "" }
|
|
return paginationDescription
|
|
}
|
|
}
|
|
|
|
open override var accessibilityElements: [Any]? {
|
|
get {
|
|
let views: [UIView] = [previousButton, collectionContainerView, nextButton]
|
|
return views.filter({ $0.isHidden == false })
|
|
}
|
|
set {
|
|
|
|
}
|
|
}
|
|
|
|
/// 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: paginationDescription)
|
|
}
|
|
}
|
|
|
|
///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)
|
|
}
|
|
}
|