forked from shadowfacts/Tusker
Extract a bunch of timeline view controller stuff to separate protocol
This commit is contained in:
parent
d18a4b3c42
commit
a682c8f5cc
|
@ -35,6 +35,7 @@
|
|||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||
|
@ -380,6 +381,7 @@
|
|||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||
|
@ -1298,6 +1300,7 @@
|
|||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */,
|
||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||
|
@ -1900,6 +1903,7 @@
|
|||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||
|
|
|
@ -14,20 +14,19 @@ import SwiftSoup
|
|||
|
||||
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
||||
|
||||
class TimelineViewController: UIViewController {
|
||||
|
||||
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController {
|
||||
let timeline: Timeline
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var controller: TimelineLikeController<TimelineItem>!
|
||||
private var confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||
private(set) var controller: TimelineLikeController<TimelineItem>!
|
||||
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||
private var newer: RequestRange?
|
||||
private var older: RequestRange?
|
||||
|
||||
private var collectionView: UICollectionView {
|
||||
var collectionView: UICollectionView {
|
||||
view as! UICollectionView
|
||||
}
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
||||
self.timeline = timeline
|
||||
|
@ -61,12 +60,12 @@ class TimelineViewController: UIViewController {
|
|||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
// TODO: delegates
|
||||
collectionView.delegate = self
|
||||
// collectionView.dragDelegate = self
|
||||
// collectionView.dragDelegate = self
|
||||
|
||||
dataSource = createDataSource()
|
||||
applyInitialSnapshot()
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction(handler: { [unowned self] _ in
|
||||
Task {
|
||||
await self.controller.loadNewer()
|
||||
|
@ -74,7 +73,7 @@ class TimelineViewController: UIViewController {
|
|||
}
|
||||
}))
|
||||
collectionView.refreshControl = refreshControl
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
@ -135,18 +134,26 @@ class TimelineViewController: UIViewController {
|
|||
}
|
||||
|
||||
extension TimelineViewController {
|
||||
enum Section: Hashable {
|
||||
enum Section: TimelineLikeCollectionViewSection {
|
||||
case header
|
||||
case statuses
|
||||
case footer
|
||||
|
||||
static var entries: Self { .statuses }
|
||||
}
|
||||
enum Item: Hashable {
|
||||
enum Item: TimelineLikeCollectionViewItem {
|
||||
typealias TimelineItem = String // status ID
|
||||
|
||||
case status(id: String, state: StatusState)
|
||||
case loadingIndicator
|
||||
case confirmLoadMore
|
||||
// // TODO: remove local param from this
|
||||
// case publicTimelineDescription(local: Bool)
|
||||
|
||||
static func fromTimelineItem(_ id: String) -> Self {
|
||||
return .status(id: id, state: .unknown)
|
||||
}
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||
|
@ -188,7 +195,8 @@ extension TimelineViewController {
|
|||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||
// MARK: - TimelineLikeControllerDelegate
|
||||
extension TimelineViewController {
|
||||
typealias TimelineItem = String // status ID
|
||||
|
||||
func loadInitial() async throws -> [TimelineItem] {
|
||||
|
@ -255,120 +263,7 @@ extension TimelineViewController: TimelineLikeControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func canLoadOlder() async -> Bool {
|
||||
if Preferences.shared.disableInfiniteScrolling {
|
||||
var snapshot = dataSource.snapshot()
|
||||
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||
snapshot.appendSections([.footer])
|
||||
}
|
||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
for await _ in self.confirmLoadMore.values {
|
||||
return true
|
||||
}
|
||||
fatalError("unreachable")
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
||||
switch event {
|
||||
case .addLoadingIndicator:
|
||||
var snapshot = dataSource.snapshot()
|
||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||
snapshot.appendSections([.footer])
|
||||
}
|
||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
snapshot.reconfigureItems([.confirmLoadMore])
|
||||
} else {
|
||||
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
||||
}
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
case .removeLoadingIndicator:
|
||||
let oldContentOffset = collectionView.contentOffset
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteSections([.footer])
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
collectionView.contentOffset = oldContentOffset
|
||||
|
||||
case .loadAllError(let error, _):
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] (toast: ToastView) in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadInitial()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .replaceAllItems(let ids, _):
|
||||
var snapshot = dataSource.snapshot()
|
||||
if snapshot.sectionIdentifiers.contains(.statuses) {
|
||||
snapshot.deleteSections([.statuses])
|
||||
}
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
case .loadNewerError(Error.allCaughtUp, _):
|
||||
var config = ToastConfiguration(title: "You're all caught up")
|
||||
config.edge = .top
|
||||
config.dismissAutomaticallyAfter = 2
|
||||
config.action = { (toast) in
|
||||
toast.dismissToast(animated: true)
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .loadNewerError(let error, _):
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] (toast: ToastView) in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadNewer()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .prependItems(let ids, _):
|
||||
var snapshot = dataSource.snapshot()
|
||||
let items = ids.map { Item.status(id: $0, state: .unknown) }
|
||||
let first = snapshot.itemIdentifiers(inSection: .statuses).first
|
||||
if let first {
|
||||
snapshot.insertItems(items, beforeItem: first)
|
||||
} else {
|
||||
snapshot.appendItems(items, toSection: .statuses)
|
||||
}
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
if let first,
|
||||
let indexPath = dataSource.indexPath(for: first) {
|
||||
// TODO: i can't tell if this actually works or not
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
||||
}
|
||||
|
||||
case .loadOlderError(let error, _):
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast: ToastView) in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadOlder()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .appendItems(let ids, _):
|
||||
var snapshot = dataSource.snapshot()
|
||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
snapshot.deleteItems([.confirmLoadMore])
|
||||
}
|
||||
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noClient
|
||||
case noNewer
|
||||
case noOlder
|
||||
|
@ -391,6 +286,3 @@ extension TimelineViewController: UICollectionViewDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: ToastableViewController {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
//
|
||||
// TimelineLikeCollectionViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/24/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeControllerDelegate, ToastableViewController {
|
||||
associatedtype Section: TimelineLikeCollectionViewSection
|
||||
associatedtype Item: TimelineLikeCollectionViewItem where Item.TimelineItem == Self.TimelineItem
|
||||
associatedtype Error: TimelineLikeCollectionViewError
|
||||
|
||||
// this needs to be an IUO because it can't be set until after the super init is called, so that self can be passed as the delegate param
|
||||
var controller: TimelineLikeController<TimelineItem>! { get }
|
||||
var confirmLoadMore: PassthroughSubject<Void, Never> { get }
|
||||
|
||||
var collectionView: UICollectionView { get }
|
||||
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
|
||||
}
|
||||
|
||||
protocol TimelineLikeCollectionViewSection: Hashable {
|
||||
static var entries: Self { get }
|
||||
static var footer: Self { get }
|
||||
}
|
||||
|
||||
protocol TimelineLikeCollectionViewItem: Hashable {
|
||||
associatedtype TimelineItem
|
||||
|
||||
static var loadingIndicator: Self { get }
|
||||
static var confirmLoadMore: Self { get }
|
||||
|
||||
static func fromTimelineItem(_ item: TimelineItem) -> Self
|
||||
}
|
||||
|
||||
// TODO: equatable might not be the best for this?
|
||||
protocol TimelineLikeCollectionViewError: Error, Equatable {
|
||||
static var allCaughtUp: Self { get }
|
||||
}
|
||||
|
||||
extension TimelineLikeCollectionViewController {
|
||||
func canLoadOlder() async -> Bool {
|
||||
if Preferences.shared.disableInfiniteScrolling {
|
||||
var snapshot = dataSource.snapshot()
|
||||
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||
snapshot.appendSections([.footer])
|
||||
}
|
||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
for await _ in confirmLoadMore.values {
|
||||
return true
|
||||
}
|
||||
fatalError("unreachable")
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
||||
switch event {
|
||||
case .addLoadingIndicator:
|
||||
await handleAddLoadingIndicator()
|
||||
case .removeLoadingIndicator:
|
||||
await handleRemoveLoadingIndicator()
|
||||
case .loadAllError(let error, _):
|
||||
await handleLoadAllError(error)
|
||||
case .replaceAllItems(let items, _):
|
||||
await handleReplaceAllItems(items)
|
||||
case .loadNewerError(let error, _):
|
||||
await handleLoadNewerError(error)
|
||||
case .prependItems(let items, _):
|
||||
await handlePrependItems(items)
|
||||
case .loadOlderError(let error, _):
|
||||
await handleLoadOlderError(error)
|
||||
case .appendItems(let items, _):
|
||||
await handleAppendItems(items)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddLoadingIndicator() async {
|
||||
var snapshot = dataSource.snapshot()
|
||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||
snapshot.appendSections([.footer])
|
||||
}
|
||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
snapshot.reconfigureItems([.confirmLoadMore])
|
||||
} else {
|
||||
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
||||
}
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func handleRemoveLoadingIndicator() async {
|
||||
let oldContentOffset = collectionView.contentOffset
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteSections([.footer])
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
// prevent the collection view from scrolling as we remove the loading indicator and add the timeline items
|
||||
collectionView.contentOffset = oldContentOffset
|
||||
}
|
||||
|
||||
func handleLoadAllError(_ error: Swift.Error) async {
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadInitial()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
|
||||
func handleReplaceAllItems(_ timelineItems: [TimelineItem]) async {
|
||||
var snapshot = dataSource.snapshot()
|
||||
if snapshot.sectionIdentifiers.contains(.entries) {
|
||||
snapshot.deleteSections([.entries])
|
||||
}
|
||||
snapshot.appendSections([.entries])
|
||||
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func handleLoadNewerError(_ error: Swift.Error) async {
|
||||
var config: ToastConfiguration
|
||||
if let error = error as? Self.Error,
|
||||
error == .allCaughtUp {
|
||||
config = ToastConfiguration(title: "You're all caught up")
|
||||
config.edge = .top
|
||||
config.dismissAutomaticallyAfter = 2
|
||||
config.action = { toast in
|
||||
toast.dismissToast(animated: true)
|
||||
}
|
||||
} else {
|
||||
config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadNewer()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
|
||||
func handlePrependItems(_ timelineItems: [TimelineItem]) async {
|
||||
let items = timelineItems.map { Item.fromTimelineItem($0) }
|
||||
var snapshot = dataSource.snapshot()
|
||||
let first = snapshot.itemIdentifiers(inSection: .entries).first
|
||||
if let first {
|
||||
snapshot.insertItems(items, beforeItem: first)
|
||||
} else {
|
||||
snapshot.appendItems(items, toSection: .entries)
|
||||
}
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
if let first,
|
||||
let indexPath = dataSource.indexPath(for: first) {
|
||||
// TODO: i can't tell if this actually works or not
|
||||
// maintain the current scroll position in the list (don't scroll to top)
|
||||
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func handleLoadOlderError(_ error: Swift.Error) async {
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadOlder()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
|
||||
func handleAppendItems(_ timelineItems: [TimelineItem]) async {
|
||||
var snapshot = dataSource.snapshot()
|
||||
// TODO: this might not be necessary, isn't the confirm item removed separately?
|
||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
snapshot.deleteItems([.confirmLoadMore])
|
||||
}
|
||||
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue