From 44021d3ad2727911d31453dfee674f04d6c142af Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 27 Oct 2023 17:13:49 -0500 Subject: [PATCH] Convert edit list screen to collection view --- .../EditListAccountsViewController.swift | 210 +++++++++++++----- 1 file changed, 152 insertions(+), 58 deletions(-) diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index 76513c6c..b3327cd5 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -10,18 +10,21 @@ import UIKit import Pachyderm import Combine -class EditListAccountsViewController: EnhancedTableViewController { +class EditListAccountsViewController: UIViewController, CollectionViewController { private var list: List - let mastodonController: MastodonController + private let mastodonController: MastodonController - var changedAccounts = false + private var state = State.unloaded - var dataSource: DataSource! - var nextRange: RequestRange? + private(set) var changedAccounts = false - var searchResultsController: SearchResultsViewController! - var searchController: UISearchController! + private var dataSource: UICollectionViewDiffableDataSource! + var collectionView: UICollectionView! { view as? UICollectionView } + private var nextRange: RequestRange? + + private var searchResultsController: SearchResultsViewController! + private var searchController: UISearchController! private var listRenamedCancellable: AnyCancellable? @@ -29,7 +32,7 @@ class EditListAccountsViewController: EnhancedTableViewController { self.list = list self.mastodonController = mastodonController - super.init(style: .plain) + super.init(nibName: nil, bundle: nil) listChanged() @@ -46,29 +49,45 @@ class EditListAccountsViewController: EnhancedTableViewController { fatalError("init(coder:) has not been implemeneted") } + override func loadView() { + var config = UICollectionLayoutListConfiguration(appearance: .grouped) + config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in + var config = sectionConfig + switch dataSource.itemIdentifier(for: indexPath)! { + case .loadingIndicator: + config.topSeparatorVisibility = .hidden + config.bottomSeparatorVisibility = .hidden + case .account(id: _): + config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets + config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets + } + return config + } + config.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in + switch dataSource.itemIdentifier(for: indexPath) { + case .account(id: let id): + let remove = UIContextualAction(style: .destructive, title: "Remove") { [unowned self] _, _, completion in + Task { + await self.removeAccount(id: id) + completion(true) + } + } + return UISwipeActionsConfiguration(actions: [remove]) + default: + return nil + } + } + let layout = UICollectionViewCompositionalLayout.list(using: config) + view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.allowsSelection = false + collectionView.backgroundColor = .appGroupedBackground + dataSource = createDataSource() + } + override func viewDidLoad() { super.viewDidLoad() - tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: "accountCell") - - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 66 - tableView.allowsSelection = false - tableView.backgroundColor = .appGroupedBackground - - dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in - guard case let .account(id) = item else { fatalError() } - - let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell - cell.delegate = self - cell.updateUI(accountID: id) - cell.configurationUpdateHandler = { cell, state in - cell.backgroundConfiguration = .appListGroupedCell(for: state) - } - return cell - }) - dataSource.editListAccountsController = self - searchResultsController = SearchResultsViewController(mastodonController: mastodonController, scope: .people) searchResultsController.following = true searchResultsController.delegate = self @@ -87,7 +106,34 @@ class EditListAccountsViewController: EnhancedTableViewController { } navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed)) + } + + private func createDataSource() -> UICollectionViewDiffableDataSource { + let loadingCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in + cell.indicator.startAnimating() + } + let accountCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, itemIdentifier in + cell.delegate = self + cell.updateUI(accountID: itemIdentifier) + cell.configurationUpdateHandler = { cell, state in + cell.backgroundConfiguration = .appListGroupedCell(for: state) + } + } + return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in + switch itemIdentifier { + case .loadingIndicator: + return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ()) + case .account(id: let id): + return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id) + } + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + clearSelectionOnAppear(animated: animated) + Task { await loadAccounts() } @@ -97,10 +143,21 @@ class EditListAccountsViewController: EnhancedTableViewController { title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title) } - func loadAccounts() async { + @MainActor + private func loadAccounts() async { + guard state == .unloaded else { return } + + state = .loading + + async let results = try await mastodonController.run(List.getAccounts(list.id)) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.accounts]) + snapshot.appendItems([.loadingIndicator]) + await dataSource.apply(snapshot) + do { - let request = List.getAccounts(list.id) - let (accounts, pagination) = try await mastodonController.run(request) + let (accounts, pagination) = try await results self.nextRange = pagination?.older await withCheckedContinuation { continuation in @@ -109,20 +166,61 @@ class EditListAccountsViewController: EnhancedTableViewController { } } - var snapshot = self.dataSource.snapshot() - if snapshot.indexOfSection(.accounts) == nil { - snapshot.appendSections([.accounts]) - } else { - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .accounts)) - } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.accounts]) snapshot.appendItems(accounts.map { .account(id: $0.id) }) await dataSource.apply(snapshot) + + state = .loaded } catch { let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in toast.dismissToast(animated: true) await self.loadAccounts() } self.showToast(configuration: config, animated: true) + + state = .unloaded + await dataSource.apply(.init()) + } + } + + private func loadNextPage() async { + guard state == .loaded, + let nextRange else { return } + + state = .loading + + async let results = try await mastodonController.run(List.getAccounts(list.id, range: nextRange)) + + let origSnapshot = dataSource.snapshot() + var snapshot = origSnapshot + snapshot.appendItems([.loadingIndicator]) + await dataSource.apply(snapshot) + + do { + let (accounts, pagination) = try await results + self.nextRange = pagination?.older + + await withCheckedContinuation { continuation in + mastodonController.persistentContainer.addAll(accounts: accounts) { + continuation.resume() + } + } + + var snapshot = origSnapshot + snapshot.appendItems(accounts.map { .account(id: $0.id) }) + await dataSource.apply(snapshot) + + state = .loaded + } catch { + let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in + toast.dismissToast(animated: true) + await self.loadNextPage() + } + self.showToast(configuration: config, animated: true) + + state = .loaded + await dataSource.apply(origSnapshot) } } @@ -157,12 +255,6 @@ class EditListAccountsViewController: EnhancedTableViewController { } } - // MARK: - Table view delegate - - override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - return .delete - } - // MARK: - Interaction @objc func renameButtonPressed() { @@ -171,29 +263,31 @@ class EditListAccountsViewController: EnhancedTableViewController { } +extension EditListAccountsViewController { + enum State { + case unloaded + case loading + case loaded + case loadingOlder + } +} + extension EditListAccountsViewController { enum Section: Hashable { case accounts } enum Item: Hashable { + case loadingIndicator case account(id: String) } - - class DataSource: UITableViewDiffableDataSource { - weak var editListAccountsController: EditListAccountsViewController? - - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return true - } - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - guard editingStyle == .delete, - case let .account(id) = itemIdentifier(for: indexPath) else { - return - } - +} + +extension EditListAccountsViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + if state == .loaded, + indexPath.item == collectionView.numberOfItems(inSection: indexPath.section) - 1 { Task { - await self.editListAccountsController?.removeAccount(id: id) + await loadNextPage() } } }