forked from shadowfacts/Tusker
Remove TimelineLikeTableViewController
Everything now uses DiffableTimelineLike
This commit is contained in:
parent
654b5d9c59
commit
2b22180191
|
@ -154,7 +154,6 @@
|
|||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
|
||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */; };
|
||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||
|
@ -559,7 +558,6 @@
|
|||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
|
||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeTableViewController.swift; sourceTree = "<group>"; };
|
||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1523,7 +1521,6 @@
|
|||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||
|
@ -2080,7 +2077,6 @@
|
|||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
|
||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
|
||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
//
|
||||
// TimelineLikeTableViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/15/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A table view controller that manages common functionality between timeline-like UIs.
|
||||
/// For example, this class handles loading new items when the user scrolls to the end,
|
||||
/// refreshing, and pruning offscreen rows automatically.
|
||||
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
||||
|
||||
private(set) var loaded = false
|
||||
|
||||
var sections: [[Item]] = []
|
||||
|
||||
private let pageSize = 20
|
||||
|
||||
private var lastLastVisibleRow: IndexPath?
|
||||
|
||||
init() {
|
||||
super.init(style: .plain)
|
||||
|
||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle()))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func item(for indexPath: IndexPath) -> Item {
|
||||
return sections[indexPath.section][indexPath.row]
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 140
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
self.refreshControl = UIRefreshControl()
|
||||
self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||
#endif
|
||||
|
||||
if let prefetchSource = self as? UITableViewDataSourcePrefetching {
|
||||
tableView.prefetchDataSource = prefetchSource
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
loadInitial()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
pruneOffscreenRows()
|
||||
}
|
||||
|
||||
func loadInitial() {
|
||||
guard !loaded else { return }
|
||||
// set loaded immediately so we don't trigger another request while the current one is running
|
||||
loaded = true
|
||||
|
||||
loadInitialItems() { (items) in
|
||||
DispatchQueue.main.async {
|
||||
guard items.count > 0 else {
|
||||
// set loaded back to false so the next time the VC appears, we try to load again
|
||||
// todo: this should probably retry automatically
|
||||
self.loaded = false
|
||||
return
|
||||
}
|
||||
|
||||
if self.sections.count < self.headerSectionsCount() {
|
||||
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
||||
}
|
||||
|
||||
self.sections.append(items)
|
||||
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadInitialItems() {
|
||||
loaded = false
|
||||
sections = []
|
||||
loadInitial()
|
||||
}
|
||||
|
||||
func cellHeightChanged() {
|
||||
// causes the table view to recalculate the cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
|
||||
class func refreshCommandTitle() -> String {
|
||||
return "Refresh"
|
||||
}
|
||||
|
||||
// todo: these three should use Result<[Item], Client.Error> so we can differentiate between failed requests and there actually being no results
|
||||
|
||||
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
|
||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
||||
}
|
||||
|
||||
func loadOlder(completion: @escaping ([Item]) -> Void) {
|
||||
fatalError("loadOlder(completion:) must be implemented by subclasses")
|
||||
}
|
||||
|
||||
func loadNewer(completion: @escaping ([Item]) -> Void) {
|
||||
fatalError("loadNewer(completion:) must be implemented by subclasses")
|
||||
}
|
||||
|
||||
func willRemoveRows(at indexPaths: [IndexPath]) {
|
||||
}
|
||||
|
||||
func headerSectionsCount() -> Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
private func pruneOffscreenRows() {
|
||||
guard let lastVisibleRow = lastLastVisibleRow,
|
||||
// never remove the last section
|
||||
sections.count - headerSectionsCount() > 1 else {
|
||||
return
|
||||
}
|
||||
let lastSectionIndex = sections.count - 1
|
||||
|
||||
if lastVisibleRow.section < lastSectionIndex {
|
||||
// if there is a section below the last visible one
|
||||
|
||||
let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex
|
||||
|
||||
let indexPathsToRemove = sectionsToRemove.flatMap { (section) in
|
||||
sections[section].indices.map { (row) in
|
||||
IndexPath(row: row, section: section)
|
||||
}
|
||||
}
|
||||
willRemoveRows(at: indexPathsToRemove)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
||||
}
|
||||
|
||||
sections.removeSubrange(sectionsToRemove)
|
||||
} else if lastVisibleRow.section == lastSectionIndex {
|
||||
let lastSection = sections.last!
|
||||
let lastRowIndex = lastSection.count - 1
|
||||
|
||||
if lastVisibleRow.row < lastRowIndex - pageSize {
|
||||
// if there are more than pageSize rows in the current section below the last visible one
|
||||
|
||||
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + pageSize)..<lastSection.count
|
||||
|
||||
let indexPathsToRemove = rowIndicesInLastSectionToRemove.map {
|
||||
IndexPath(row: $0, section: lastSectionIndex)
|
||||
}
|
||||
willRemoveRows(at: indexPathsToRemove)
|
||||
|
||||
sections[lastSectionIndex].removeSubrange(rowIndicesInLastSectionToRemove)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteRows(at: indexPathsToRemove, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return sections.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return sections[section].count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
fatalError("tableView(_:cellForRowAt:) must be implemented by subclasses")
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// this assumes that indexPathsForVisibleRows is always in order
|
||||
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
||||
|
||||
if indexPath.section == sections.count - 1,
|
||||
indexPath.row == sections[indexPath.section].count - 1 {
|
||||
loadOlder() { (newItems) in
|
||||
guard newItems.count > 0 else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let newRows = self.sections.last!.count..<(self.sections.last!.count + newItems.count)
|
||||
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.sections.count - 1) }
|
||||
|
||||
self.sections[self.sections.count - 1].append(contentsOf: newItems)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||
}
|
||||
|
||||
// MARK: - RefreshableViewController
|
||||
func refresh() {
|
||||
loadNewer() { (newItems) in
|
||||
DispatchQueue.main.async {
|
||||
self.refreshControl?.endRefreshing()
|
||||
|
||||
guard newItems.count > 0 else { return }
|
||||
|
||||
let firstNonHeaderSection = self.headerSectionsCount()
|
||||
|
||||
self.sections[firstNonHeaderSection].insert(contentsOf: newItems, at: 0)
|
||||
|
||||
let newIndexPaths = (0..<newItems.count).map { IndexPath(row: $0, section: firstNonHeaderSection) }
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
||||
}
|
||||
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
self.tableView.scrollToRow(at: IndexPath(row: newItems.count, section: firstNonHeaderSection), at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineLikeTableViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
pruneOffscreenRows()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue