chromium/ios/chrome/browser/spotlight_debugger/ui_bundled/spotlight_debugger_all_items_view_controller.swift

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import CoreSpotlight
import UIKit

/// Controller backed by SpotlightLogger known items, with filter function.
class ItemsController {

  var query: CSSearchQuery? = nil
  var allItems = [CSSearchableItem]()

  func filteredItems(with filter: String? = nil, limit: Int? = nil) -> [CSSearchableItem] {
    let filtered = allItems.filter { $0.contains(filter) }
    if let limit = limit {
      return Array(filtered.prefix(through: limit))
    } else {
      return filtered
    }
  }

  func fetchAllItems(completionHandler: @escaping () -> Void) {
    self.allItems = []
    let queryString = "title == *"
    let context = CSSearchQueryContext()
    context.fetchAttributes = [
      "uniqueIdentifier",
      "title", "domain", "id", "URL", "description",
      "thumbnail data", "displayName", "keywords",
      "contentType", "domainIdentifier", "identifier", "rankingHint",
    ]
    self.query = CSSearchQuery(
      queryString: queryString,
      queryContext: context)
    self.query?.foundItemsHandler = { (items: [CSSearchableItem]) -> Void in
      self.allItems.append(contentsOf: items)
    }
    self.query?.completionHandler = { (error: Error?) -> Void in
      DispatchQueue.main.async {
        completionHandler()
      }
    }
    self.query?.start()
  }
}

/// Displays a list of all searchable items from the spotlight logger.
class SpotlightDebuggerAllItemsViewController: UIViewController {

  enum Section: CaseIterable {
    case main
  }
  let itemsController = ItemsController()
  let searchBar = UISearchBar(frame: .zero)
  var collectionView: UICollectionView!
  var dataSource: UICollectionViewDiffableDataSource<Section, CSSearchableItem>!
  var nameFilter: String?

  override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.title = "Donated Items"
    configureHierarchy()
    configureDataSource()
    performQuery(with: nil)
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    collectionView.deselectAllItems(animated: animated)
    itemsController.fetchAllItems {
      // Reload data by executing an empty filter query.
      self.performQuery(with: "")
    }
  }
}

extension UICollectionView {
  func deselectAllItems(animated: Bool) {
    guard let selectedItems = indexPathsForSelectedItems else { return }
    for indexPath in selectedItems { deselectItem(at: indexPath, animated: animated) }
  }
}

extension SpotlightDebuggerAllItemsViewController {
  func configureDataSource() {

    let cellRegistration = UICollectionView.CellRegistration<
      UICollectionViewListCell, CSSearchableItem
    > { (cell, indexPath, item) in
      var content = cell.defaultContentConfiguration()
      content.text = item.attributeSet.title
      if let data = item.attributeSet.thumbnailData {
        content.image = UIImage(data: data)
        content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
      } else {
        content.image = UIImage(systemName: "questionmark.diamond")
      }

      cell.accessories = [.disclosureIndicator()]

      cell.contentConfiguration = content
    }

    dataSource = UICollectionViewDiffableDataSource<Section, CSSearchableItem>(
      collectionView: collectionView
    ) {
      (collectionView: UICollectionView, indexPath: IndexPath, identifier: CSSearchableItem)
        -> UICollectionViewCell? in
      // Return the cell.
      return collectionView.dequeueConfiguredReusableCell(
        using: cellRegistration, for: indexPath, item: identifier)
    }
  }

  func performQuery(with filter: String?) {
    let items = itemsController.filteredItems(with: filter).sorted {
      $0.uniqueIdentifier < $1.uniqueIdentifier
    }

    var snapshot = NSDiffableDataSourceSnapshot<Section, CSSearchableItem>()
    snapshot.appendSections([.main])
    snapshot.appendItems(items)
    dataSource.apply(snapshot, animatingDifferences: true)
  }
}

extension SpotlightDebuggerAllItemsViewController {
  func createLayout() -> UICollectionViewLayout {
    let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    return UICollectionViewCompositionalLayout.list(using: config)
  }

  func configureHierarchy() {
    view.backgroundColor = .systemBackground
    let layout = createLayout()
    let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    searchBar.translatesAutoresizingMaskIntoConstraints = false
    collectionView.backgroundColor = .systemBackground
    collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    collectionView.delegate = self
    view.addSubview(collectionView)
    view.addSubview(searchBar)

    let views = ["cv": collectionView, "searchBar": searchBar]
    var constraints = [NSLayoutConstraint]()
    constraints.append(
      contentsOf: NSLayoutConstraint.constraints(
        withVisualFormat: "H:|[cv]|", options: [], metrics: nil, views: views))
    constraints.append(
      contentsOf: NSLayoutConstraint.constraints(
        withVisualFormat: "H:|[searchBar]|", options: [], metrics: nil, views: views))
    constraints.append(
      contentsOf: NSLayoutConstraint.constraints(
        withVisualFormat: "V:[searchBar]-20-[cv]|", options: [], metrics: nil, views: views))
    constraints.append(
      searchBar.topAnchor.constraint(
        equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1.0))
    NSLayoutConstraint.activate(constraints)
    self.collectionView = collectionView

    searchBar.delegate = self
  }
}

extension SpotlightDebuggerAllItemsViewController: UICollectionViewDelegate {
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
      collectionView.deselectItem(at: indexPath, animated: true)
      return
    }
    let detailViewController = SearchableItemDetailViewController(with: item)
    self.navigationController?.pushViewController(detailViewController, animated: true)
  }
}

extension SpotlightDebuggerAllItemsViewController: UISearchBarDelegate {
  func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    performQuery(with: searchText)
  }
}

extension CSSearchableItem {

  func contains(_ filter: String?) -> Bool {
    guard let filterText = filter else { return true }
    if filterText.isEmpty { return true }
    let lowercasedFilter = filterText.lowercased()

    return (attributeSet.title?.lowercased().contains(lowercasedFilter) ?? false)
      || (attributeSet.url?.absoluteString.lowercased().contains(lowercasedFilter) ?? false)
      || (domainIdentifier?.lowercased().contains(lowercasedFilter) ?? false)
      || uniqueIdentifier.lowercased().contains(lowercasedFilter)
  }
}