// // Breadcrumbs.swift // VDS // // Created by Kanamarlapudi, Vasavi on 11/03/24. // import Foundation import UIKit import VDSCoreTokens import Combine /// A Breadcrumbs contains BreadcrumbItems. /// It contains Breadcrumb Item Default, Breadcrumb Item Selected, Separator. /// Breadcrumbs are secondary navigation that use a hierarchy of internal links to tell customers where they are in an experience. Each breadcrumb links to its respective page, except for that of current page. @objcMembers @objc(VDSBreadcrumbs) open class Breadcrumbs: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Array of ``BreadcrumbItem`` views for the Breadcrumbs. open var breadcrumbs: [BreadcrumbItem] = [] { didSet { setNeedsUpdate() } } /// Array of ``BreadcurmbItemModel`` you are wanting to show. open var breadcrumbModels: [BreadcrumbItemModel] = [] { didSet { updateBreadcrumbItems() } } /// Whether this object is enabled or not override open var isEnabled: Bool { didSet { breadcrumbs.forEach { $0.isEnabled = isEnabled } } } open override var accessibilityElements: [Any]? { get { return [containerView, breadcrumbs] } set {} } /// A callback when the selected item changes. Passes parameters (crumb). open var onBreadcrumbDidSelect: ((BreadcrumbItem) -> Void)? /// A callback when the Tab determine if a item should be selected. open var onBreadcrumbShouldSelect:((BreadcrumbItem) -> Bool)? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- fileprivate lazy var layout = ButtonGroupPositionLayout().with { $0.position = .left $0.delegate = self $0.axisSpacer = { _, _, _ in return VDSLayout.space1X } $0.verticalSpacer = { _, _ in return VDSLayout.space1X } } ///Collectionview to render Breadcrumb Items private lazy var collectionView: SelfSizingCollectionView = { let collectionView = SelfSizingCollectionView(frame: .zero, collectionViewLayout: layout) collectionView.isScrollEnabled = false collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.delegate = self collectionView.dataSource = self collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false collectionView.register(BreadcrumbCellItem.self, forCellWithReuseIdentifier: BreadcrumbCellItem.identifier) collectionView.backgroundColor = .clear return collectionView }() private let containerView = View().with { $0.isAccessibilityElement = true $0.accessibilityLabel = "Breadcrumbs" } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- /// Removes all of the Breadcrumbs and creates new ones from the Breadcrumb Models property. private func updateBreadcrumbItems() { // Clear existing breadcrumbs for breadcrumbItem in breadcrumbs { breadcrumbItem.removeFromSuperview() } // Create new breadcrumb items from the models breadcrumbs = breadcrumbModels.compactMap({ model in let breadcrumbItem = BreadcrumbItem() breadcrumbItem.text = model.text breadcrumbItem.isSelected = model.selected breadcrumbItem.onClick = { [weak self] breadcrumb in guard let self, breadcrumb.isEnabled else { return } if self.onBreadcrumbShouldSelect?(breadcrumb) ?? true { model.onClick?(breadcrumb) self.onBreadcrumbDidSelect?(breadcrumb) } } return breadcrumbItem }) } //------------------------------------------s-------- // MARK: - Overrides //-------------------------------------------------- /// Executed on initialization for this View. open override func setup() { super.setup() containerView.addSubview(collectionView) collectionView.pinToSuperView() addSubview(containerView) containerView.pinToSuperView() } /// Resets to default settings. open override func reset() { super.reset() breadcrumbs.forEach { $0.reset() } } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() collectionView.reloadData() } open override func layoutSubviews() { //Turn off the ability to execute updateView() in the super //since we don't want an infinite loop shouldUpdateView = false super.layoutSubviews() shouldUpdateView = true // Accounts for any collection size changes DispatchQueue.main.async { [weak self] in guard let self else { return } self.collectionView.collectionViewLayout.invalidateLayout() } } private var separatorWidth = Label().with { $0.text = "/"; $0.sizeToFit() }.intrinsicContentSize.width } extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource, ButtongGroupPositionLayoutDelegate { //-------------------------------------------------- // MARK: - UICollectionView Delegate & Datasource //-------------------------------------------------- public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { breadcrumbs.count } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BreadcrumbCellItem.identifier, for: indexPath) as? BreadcrumbCellItem else { return UICollectionViewCell() } let breadcrumb = breadcrumbs[indexPath.row] breadcrumb.hideSlash = breadcrumb == breadcrumbs.first breadcrumb.surface = surface cell.update(breadCrumbItem: breadcrumb) return cell } public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { let breadcrumb = breadcrumbs[indexPath.row] breadcrumb.hideSlash = breadcrumb == breadcrumbs.first let maxWidth = frame.width let intrinsicSize = breadcrumb.titleLabel!.sizeThatFits(.init(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)) let cellwidth = min(maxWidth, intrinsicSize.width) return .init(width: cellwidth, height: intrinsicSize.height) } public func collectionView(_ collectionView: UICollectionView, buttonBaseAtIndexPath indexPath: IndexPath) -> ButtonBase { breadcrumbs[indexPath.row] } }