// // Breadcrumbs.swift // VDS // // Created by Kanamarlapudi, Vasavi on 11/03/24. // import Foundation import VDSColorTokens 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. @objc(VDSBreadcrumbs) open class Breadcrumbs: View { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Array of ``BreadcrumbItem`` views for the Breadcrumbs. open var breadcrumbs: [BreadcrumbItem] = [] /// 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 } } } /// Current Surface and this is used to pass down to child objects that implement Surfacable override open var surface: Surface { didSet { breadcrumbs.forEach { $0.surface = surface } } } /// 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 //-------------------------------------------------- // Sizes are from InVision design specs. internal var containerSize: CGSize { CGSize(width: 45, height: 44) } internal var heightConstraint: NSLayoutConstraint? let layout: UICollectionViewFlowLayout = LeftAlignedCollectionViewFlowLayout().with { $0.estimatedItemSize = UICollectionViewFlowLayout.automaticSize $0.minimumInteritemSpacing = VDSLayout.Spacing.space1X.value $0.minimumLineSpacing = VDSLayout.Spacing.space1X.value $0.sectionInset = .zero $0.scrollDirection = .vertical } ///Collectionview to render Breadcrumb Items private lazy var collectionView: UICollectionView = { 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(BreadcrumbCellItem.self, forCellWithReuseIdentifier: BreadcrumbCellItem.identifier) collectionView.backgroundColor = .clear return collectionView }() //-------------------------------------------------- // 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() } breadcrumbs.removeAll() // Create new breadcrumb items from the models for model in breadcrumbModels { let breadcrumbItem = BreadcrumbItem() breadcrumbItem.text = model.text breadcrumbItem.link = model.link breadcrumbItem.isSelected = model.isSelected ?? false breadcrumbs.append(breadcrumbItem) breadcrumbItem.onClick = { [weak self] breadcrumb in guard let self else { return } if self.onBreadcrumbShouldSelect?(breadcrumb) ?? true { model.onClick?(breadcrumb) self.onBreadcrumbDidSelect?(breadcrumb) } } } setNeedsUpdate() } //------------------------------------------s-------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() //create the wrapping view heightConstraint = self.heightAnchor.constraint(equalToConstant: containerSize.height) heightConstraint?.priority = .defaultHigh heightConstraint?.isActive = true } /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() addSubview(collectionView) collectionView.pinToSuperView() } /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false breadcrumbs.forEach { $0.reset() } shouldUpdateView = true setNeedsUpdate() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() collectionView.reloadData() heightConstraint?.constant = collectionView.collectionViewLayout.collectionViewContentSize.height heightConstraint?.isActive = true } open override func layoutSubviews() { super.layoutSubviews() // Accounts for any collection size changes DispatchQueue.main.async { [weak self] in guard let self else { return } self.collectionView.collectionViewLayout.invalidateLayout() } } } extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource { //-------------------------------------------------- // 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 hideSlash = (indexPath.row == breadcrumbs.count - 1 || breadcrumbs.count == 1) cell.update(surface: surface, hideSlash: hideSlash, breadCrumbItem: breadcrumbs[indexPath.row]) return cell } public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { breadcrumbs[indexPath.row].intrinsicContentSize } }