// // Pagination.swift // VDS // // Created by Bandaru, Krishna Kishore on 01/03/24. // import Foundation import VDSColorTokens import Combine @objc(VDSPagination) open class Pagination: View { @Published var onPreviousTapped: PassthroughSubject = PassthroughSubject() @Published var onNextTapped: PassthroughSubject = PassthroughSubject() @Published var onPageWillChange: PassthroughSubject = PassthroughSubject() @Published var onPageChanged: PassthroughSubject = PassthroughSubject() public var total: Int = 0 { didSet { setNeedsUpdate() } } public var selectedPage: Int = 0 { didSet { setNeedsUpdate() } } private var numberOfRows: Int = 0 { didSet { collectionView.collectionViewLayout.invalidateLayout() setNeedsUpdate() } } private let pageItemCellSize: CGSize = .init(width: 20, height: 16) private let spacingBetweenCell: CGFloat = VDSLayout.Spacing.space1X.value private let buttonTintColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite) private let buttonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite) private lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.itemSize = pageItemCellSize layout.scrollDirection = .horizontal layout.minimumInteritemSpacing = spacingBetweenCell layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize layout.minimumLineSpacing = spacingBetweenCell layout.sectionInset = .zero let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.isScrollEnabled = false collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.delegate = self collectionView.dataSource = self collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false collectionView.register(PaginationCellItem.self, forCellWithReuseIdentifier: PaginationCellItem.identifier) collectionView.backgroundColor = .clear return collectionView }() //TODO: Need to check with textStyle with Matt as its getter only in ButtonBase private lazy var previousButton: ButtonBase = { let previousButton: ButtonBase if #available(iOS 15.0, *) { var configuration = ButtonBase.Configuration.plain() configuration.imagePadding = VDSLayout.Spacing.space2X.value configuration.attributedTitle = AttributedString("Previous", attributes: AttributeContainer([NSAttributedString.Key.font: TextStyle.boldBodySmall.font])) configuration.titleAlignment = .leading configuration.imagePlacement = .leading configuration.contentInsets = .zero previousButton = ButtonBase(configuration: configuration) } else { previousButton = ButtonBase() previousButton.imageEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: VDSLayout.Spacing.space2X.value) previousButton.setTitle("Previous", for: .normal) previousButton.titleLabel?.font = TextStyle.boldBodySmall.font } previousButton.contentHorizontalAlignment = .leading previousButton.translatesAutoresizingMaskIntoConstraints = false previousButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) previousButton.setImage(BundleManager.shared.image(for: "pagination-arrow-left")?.withRenderingMode(.alwaysTemplate), for: .normal) return previousButton }() private let nextButton: ButtonBase = { let nextButton: ButtonBase if #available(iOS 15.0, *) { var configuration = ButtonBase.Configuration.plain() configuration.imagePadding = VDSLayout.Spacing.space2X.value configuration.attributedTitle = AttributedString("Next", attributes: AttributeContainer([NSAttributedString.Key.font: TextStyle.boldBodySmall.font])) configuration.imagePlacement = .trailing configuration.titleAlignment = .trailing configuration.contentInsets = .zero nextButton = ButtonBase(configuration: configuration) } else { nextButton = ButtonBase() nextButton.imageEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: VDSLayout.Spacing.space2X.value) nextButton.semanticContentAttribute = .forceRightToLeft nextButton.titleLabel?.font = TextStyle.boldBodySmall.font nextButton.setTitle("Next", for: .normal) } //nextButton.textStyle = .boldBodySmall nextButton.translatesAutoresizingMaskIntoConstraints = false nextButton.contentHorizontalAlignment = .trailing nextButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) nextButton.setImage(BundleManager.shared.image(for: "pagination-arrow-right")?.withRenderingMode(.alwaysTemplate), for: .normal) return nextButton }() private let containerView: View = View().with { $0.translatesAutoresizingMaskIntoConstraints = false } open override func initialSetup() { super.initialSetup() addSubview(containerView) containerView.pinToSuperView() containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 288).activate() containerView.heightAnchor.constraint(equalToConstant: 44).activate() containerView.addSubview(previousButton) containerView.addSubview(collectionView) containerView.addSubview(nextButton) previousButton .pinTop() .pinBottom() .pinLeading() previousButton.trailingAnchor.constraint(greaterThanOrEqualTo: collectionView.leadingAnchor).activate() collectionView.heightAnchor.constraint(equalToConstant: VDSLayout.Spacing.space4X.value).activate() collectionView.centerYAnchor.constraint(equalTo: centerYAnchor).activate() collectionView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() collectionView.trailingAnchor.constraint(greaterThanOrEqualTo: nextButton.leadingAnchor).activate() collectionView.widthAnchor.constraint(equalToConstant: 92).activate() nextButton .pinTop() .pinBottom() .pinTrailing() nextButton.onClick = onbuttonTapped previousButton.onClick = onbuttonTapped previousButton.isHidden = true } open override func updateView() { super.updateView() previousButton.tintColor = buttonTintColorConfiguration.getColor(surface) nextButton.tintColor = buttonTintColorConfiguration.getColor(surface) previousButton.setTitleColor(buttonTextColorConfiguration.getColor(surface), for: .normal) nextButton.setTitleColor(buttonTextColorConfiguration.getColor(surface), for: .normal) collectionView.reloadData() } private func onbuttonTapped(_ sender: UIButton) { let isNextAction = sender == nextButton if isNextAction { selectedPage += 1 } else { selectedPage -= 1 } updateSelection() } private func updateSelection() { collectionView.scrollToItem(at: IndexPath(row: max(selectedPage-1, 0), section: 0), at: .left, animated: false) previousButton.isHidden = selectedPage == 0 nextButton.isHidden = selectedPage == total - 1 } } extension Pagination: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 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(selectedPage, currentIndex: indexPath.row, surface: surface) return cell } public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { onPageWillChange.send(selectedPage) return true } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedPage = indexPath.row updateSelection() onPageChanged.send(indexPath.row) } } internal final class PaginationCellItem: UICollectionViewCell { static let identifier: String = String(describing: PaginationCellItem.self) private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) private var indexLabel: Label = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center $0.numberOfLines = 1 } override init(frame: CGRect) { super.init(frame: frame) setUp() } required init?(coder: NSCoder) { super.init(coder: coder) setUp() } private func setUp() { let containerView = View() containerView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(indexLabel) contentView.addSubview(containerView) containerView.pinToSuperView() indexLabel.pinToSuperView() indexLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: VDSLayout.Spacing.space5X.value).activate() contentView.backgroundColor = .clear containerView.backgroundColor = .clear indexLabel.backgroundColor = .clear } internal func update(_ selectedIndex: Int, currentIndex: Int, surface: Surface) { indexLabel.textStyle = selectedIndex == currentIndex ? .boldBodySmall : .bodySmall indexLabel.text = "\(currentIndex)" indexLabel.textColor = textColorConfiguration.getColor(surface) } }