From e40f4faa8e85b42b4d5267ebe6ee51d444323330 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 5 Nov 2022 15:11:00 -0400 Subject: [PATCH] Rewrite TrendingStatusesViewController to use collection view --- .../TrendingStatusesViewController.swift | 172 ++++++++++++++---- 1 file changed, 134 insertions(+), 38 deletions(-) diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index fac5adf2..c640bc9c 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -9,78 +9,164 @@ import UIKit import Pachyderm -class TrendingStatusesViewController: EnhancedTableViewController { +class TrendingStatusesViewController: UIViewController { weak var mastodonController: MastodonController! - private var dataSource: UITableViewDiffableDataSource! + private var collectionView: UICollectionView { + view as! UICollectionView + } + private var dataSource: UICollectionViewDiffableDataSource! init(mastodonController: MastodonController) { self.mastodonController = mastodonController - super.init(style: .grouped) + super.init(nibName: nil, bundle: nil) - dragEnabled = true + title = NSLocalizedString("Trending Posts", comment: "trending posts screen title") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { - super.viewDidLoad() - - title = NSLocalizedString("Trending Posts", comment: "trending posts screen title") + override func loadView() { + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.leadingSwipeActionsConfigurationProvider = { [unowned self] in + (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() + } + config.trailingSwipeActionsConfigurationProvider = { [unowned self] in + (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions() + } + config.itemSeparatorHandler = { [unowned self] indexPath, sectionSeparatorConfiguration in + guard let item = self.dataSource.itemIdentifier(for: indexPath) else { + return sectionSeparatorConfiguration + } + var config = sectionSeparatorConfiguration + if item.hideSeparators { + config.topSeparatorVisibility = .hidden + config.bottomSeparatorVisibility = .hidden + } + if case .status(_, _) = item { + config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets + config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets + } + return config + } + let layout = UICollectionViewCompositionalLayout.list(using: config) + view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dragDelegate = self - tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell") - tableView.estimatedRowHeight = 144 - - dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in - let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell + dataSource = createDataSource() + } + + private func createDataSource() -> UICollectionViewDiffableDataSource { + let statusCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in cell.delegate = self - cell.updateUI(statusID: item.id, state: item.state) - return cell - }) + cell.updateUI(statusID: item.0, state: item.1) + } + let loadingCell = UICollectionView.CellRegistration { cell, _, _ in + cell.indicator.startAnimating() + } + return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in + switch itemIdentifier { + case .status(id: let id, state: let state): + return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state)) + case .loadingIndicator: + return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ()) + } + } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - let request = Client.getTrendingStatuses() + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.statuses]) + snapshot.appendItems([.loadingIndicator]) + dataSource.apply(snapshot, animatingDifferences: false) + Task { - guard let (statuses, _) = try? await mastodonController.run(request) else { - return - } - mastodonController.persistentContainer.addAll(statuses: statuses) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.statuses]) - snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }) - self.dataSource.apply(snapshot) - } + await loadTrendingStatuses() } } - // MARK: - Table View Delegate + private func loadTrendingStatuses() async { + let statuses: [Status] + do { + statuses = try await mastodonController.run(Client.getTrendingStatuses()).0 + } catch { + var snapshot = NSDiffableDataSourceSnapshot() + await dataSource.apply(snapshot) + let config = ToastConfiguration(from: error, with: "Loading Trending Posts", in: self) { toast in + toast.dismissToast(animated: true) + await self.loadTrendingStatuses() + } + showToast(configuration: config, animated: true) + return + } + await mastodonController.persistentContainer.addAll(statuses: statuses) + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.statuses]) + snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }) + await dataSource.apply(snapshot) + } } extension TrendingStatusesViewController { enum Section { case statuses } - struct Item: Hashable { - let id: String - let state: StatusState + enum Item: Hashable { + case status(id: String, state: StatusState) + case loadingIndicator - static func ==(lhs: Item, rhs: Item) -> Bool { - return lhs.id == rhs.id + var hideSeparators: Bool { + if case .loadingIndicator = self { + return true + } else { + return false + } } - func hash(into hasher: inout Hasher) { - hasher.combine(id) + var isSelectable: Bool { + if case .status(id: _, state: _) = self { + return true + } else { + return false + } } } } +extension TrendingStatusesViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + return dataSource.itemIdentifier(for: indexPath)?.isSelectable ?? false + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard case .status(id: let id, state: let state) = dataSource.itemIdentifier(for: indexPath) else { + return + } + selected(status: id, state: state.copy()) + } + + func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration() + } + + func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) + } +} + +extension TrendingStatusesViewController: UICollectionViewDragDelegate { + func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? [] + } +} + extension TrendingStatusesViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } } @@ -91,9 +177,19 @@ extension TrendingStatusesViewController: ToastableViewController { extension TrendingStatusesViewController: MenuActionProvider { } -extension TrendingStatusesViewController: StatusTableViewCellDelegate { - func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { - tableView.beginUpdates() - tableView.endUpdates() +extension TrendingStatusesViewController: StatusCollectionViewCellDelegate { + func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) { + if let indexPath = collectionView.indexPath(for: cell) { + var snapshot = dataSource.snapshot() + snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!]) + dataSource.apply(snapshot, animatingDifferences: animated, completion: completion) + } + } +} + +extension TrendingStatusesViewController: StatusBarTappableViewController { + func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + collectionView.scrollToTop() + return .stop } }