Compare commits

..

2 Commits

Author SHA1 Message Date
Shadowfacts e40f4faa8e Rewrite TrendingStatusesViewController to use collection view 2022-11-05 15:13:20 -04:00
Shadowfacts b56c6c37ec Fix crash when ProfileHeaderView tries to create observers after ProfileVC is deinit'd
Can happen if the network is slow and the user closes the profile screen before the header loads
2022-11-05 14:42:40 -04:00
2 changed files with 139 additions and 38 deletions

View File

@ -9,77 +9,163 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class TrendingStatusesViewController: EnhancedTableViewController { class TrendingStatusesViewController: UIViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var dataSource: UITableViewDiffableDataSource<Section, Item>! private var collectionView: UICollectionView {
view as! UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func viewDidLoad() { override func loadView() {
super.viewDidLoad() 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
title = NSLocalizedString("Trending Posts", comment: "trending posts screen title") dataSource = createDataSource()
}
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell") private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
tableView.estimatedRowHeight = 144 let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, StatusState)> { [unowned self] cell, indexPath, item in
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: item.id, state: item.state) cell.updateUI(statusID: item.0, state: item.1)
return cell }
}) let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { 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) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
let request = Client.getTrendingStatuses()
Task {
guard let (statuses, _) = try? await mastodonController.run(request) else {
return
}
mastodonController.persistentContainer.addAll(statuses: statuses) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.statuses]) snapshot.appendSections([.statuses])
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }) snapshot.appendItems([.loadingIndicator])
self.dataSource.apply(snapshot) dataSource.apply(snapshot, animatingDifferences: false)
}
Task {
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<Section, Item>()
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<Section, Item>()
snapshot.appendSections([.statuses])
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) })
await dataSource.apply(snapshot)
}
} }
extension TrendingStatusesViewController { extension TrendingStatusesViewController {
enum Section { enum Section {
case statuses case statuses
} }
struct Item: Hashable { enum Item: Hashable {
let id: String case status(id: String, state: StatusState)
let state: StatusState case loadingIndicator
static func ==(lhs: Item, rhs: Item) -> Bool { var hideSeparators: Bool {
return lhs.id == rhs.id if case .loadingIndicator = self {
return true
} else {
return false
}
} }
func hash(into hasher: inout Hasher) { var isSelectable: Bool {
hasher.combine(id) 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 { extension TrendingStatusesViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController } var apiController: MastodonController! { mastodonController }
@ -91,9 +177,19 @@ extension TrendingStatusesViewController: ToastableViewController {
extension TrendingStatusesViewController: MenuActionProvider { extension TrendingStatusesViewController: MenuActionProvider {
} }
extension TrendingStatusesViewController: StatusTableViewCellDelegate { extension TrendingStatusesViewController: StatusCollectionViewCellDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
tableView.beginUpdates() if let indexPath = collectionView.indexPath(for: cell) {
tableView.endUpdates() 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
} }
} }

View File

@ -86,6 +86,11 @@ class ProfileHeaderView: UIView {
} }
private func createObservers() { private func createObservers() {
// mastodonController may be nil if the ProfileViewController is deinit'd before the header is even created
guard let mastodonController else {
return
}
cancellables = [] cancellables = []
mastodonController.persistentContainer.accountSubject mastodonController.persistentContainer.accountSubject