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 */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.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 */; };
|
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1523,7 +1521,6 @@
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
|
@ -2080,7 +2077,6 @@
|
||||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
|
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
|
||||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
|
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.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