Add loading indicator to DiffableTimelineLikeTableViewController

This commit is contained in:
Shadowfacts 2022-09-12 21:52:10 -04:00
parent 8b78a5e7ad
commit bbfb3b0a7a
6 changed files with 157 additions and 50 deletions

View File

@ -50,6 +50,7 @@
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; }; D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; }; D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; };
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; }; D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; };
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */; };
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; }; D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; }; D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; }; D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; };
@ -397,6 +398,7 @@
D623A5402635FB3C0095BD04 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; }; D623A5402635FB3C0095BD04 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = "<group>"; }; D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = "<group>"; };
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; }; D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = "<group>"; };
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; }; D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; }; D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; };
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = "<group>"; }; D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = "<group>"; };
@ -1268,6 +1270,7 @@
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */, D6DD2A44273D6C5700386A6C /* GIFImageView.swift */,
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */, D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
D620483323D3801D008A63EF /* LinkTextView.swift */, D620483323D3801D008A63EF /* LinkTextView.swift */,
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */,
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */, D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */, D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */, D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
@ -1918,6 +1921,7 @@
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */, D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */, D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationGroup> { class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationsTableViewController.Item> {
private let statusCell = "statusCell" private let statusCell = "statusCell"
private let actionGroupCell = "actionGroupCell" private let actionGroupCell = "actionGroupCell"
@ -56,7 +56,12 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
// MARK: - DiffableTimelineLikeTableViewController // MARK: - DiffableTimelineLikeTableViewController
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ group: NotificationGroup) -> UITableViewCell? { override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
if case .loadingIndicator = item {
return self.loadingIndicatorCell(indexPath: indexPath)
}
let group = item.group!
switch group.kind { switch group.kind {
case .mention: case .mention:
guard let notification = group.notifications.first, guard let notification = group.notifications.first,
@ -118,7 +123,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
self.mastodonController.persistentContainer.addAll(notifications: notifications) { self.mastodonController.persistentContainer.addAll(notifications: notifications) {
var snapshot = Snapshot() var snapshot = Snapshot()
snapshot.appendSections([.notifications]) snapshot.appendSections([.notifications])
snapshot.appendItems(groups, toSection: .notifications) snapshot.appendItems(groups.map { .notificationGroup($0) }, toSection: .notifications)
completion(.success(snapshot)) completion(.success(snapshot))
} }
} }
@ -145,11 +150,11 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) { self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
let existingGroups = currentSnapshot().itemIdentifiers let existingGroups = currentSnapshot().itemIdentifiers.compactMap(\.group)
let merged = NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes) let merged = NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes)
var snapshot = NSDiffableDataSourceSnapshot<Section, NotificationGroup>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.notifications]) snapshot.appendSections([.notifications])
snapshot.appendItems(merged, toSection: .notifications) snapshot.appendItems(merged.map { .notificationGroup($0) }, toSection: .notifications)
completion(.success(snapshot)) completion(.success(snapshot))
} }
} }
@ -179,11 +184,11 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) { self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
let existingGroups = currentSnapshot().itemIdentifiers let existingGroups = currentSnapshot().itemIdentifiers.compactMap(\.group)
let merged = NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes) let merged = NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes)
var snapshot = NSDiffableDataSourceSnapshot<Section, NotificationGroup>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.notifications]) snapshot.appendSections([.notifications])
snapshot.appendItems(merged, toSection: .notifications) snapshot.appendItems(merged.map { .notificationGroup($0) }, toSection: .notifications)
completion(.success(snapshot)) completion(.success(snapshot))
} }
} }
@ -191,9 +196,12 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
} }
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) { private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return } guard let item = dataSource.itemIdentifier(for: indexPath),
let notifications = item.group?.notifications else {
return
}
let group = DispatchGroup() let group = DispatchGroup()
item.notifications notifications
.map { Pachyderm.Notification.dismiss(id: $0.id) } .map { Pachyderm.Notification.dismiss(id: $0.id) }
.forEach { (request) in .forEach { (request) in
group.enter() group.enter()
@ -241,9 +249,23 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
} }
extension NotificationsTableViewController { extension NotificationsTableViewController {
enum Section: CaseIterable, Hashable { enum Section: DiffableTimelineLikeSection {
case loadingIndicator
case notifications case notifications
} }
enum Item: DiffableTimelineLikeItem {
case loadingIndicator
case notificationGroup(NotificationGroup)
var group: NotificationGroup? {
switch self {
case .loadingIndicator:
return nil
case .notificationGroup(let group):
return group
}
}
}
} }
extension NotificationsTableViewController: TuskerNavigationDelegate { extension NotificationsTableViewController: TuskerNavigationDelegate {
@ -265,7 +287,7 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
extension NotificationsTableViewController: UITableViewDataSourcePrefetching { extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue } guard let group = dataSource.itemIdentifier(for: indexPath)?.group else { continue }
for notification in group.notifications { for notification in group.notifications {
guard let avatar = notification.account.avatar else { continue } guard let avatar = notification.account.avatar else { continue }
ImageCache.avatars.fetchIfNotCached(avatar) ImageCache.avatars.fetchIfNotCached(avatar)
@ -275,7 +297,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue } guard let group = dataSource.itemIdentifier(for: indexPath)?.group else { continue }
for notification in group.notifications { for notification in group.notifications {
guard let avatar = notification.account.avatar else { continue } guard let avatar = notification.account.avatar else { continue }
ImageCache.avatars.cancelWithoutCallback(avatar) ImageCache.avatars.cancelWithoutCallback(avatar)

View File

@ -60,14 +60,17 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
// MARK: - DiffableTimelineLikeTableViewController // MARK: - DiffableTimelineLikeTableViewController
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? { override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell switch item {
case .loadingIndicator:
cell.delegate = self return self.loadingIndicatorCell(indexPath: indexPath)
// todo: dataSource.sectionIdentifier is only available on iOS 15
cell.showPinned = dataSource.snapshot().indexOfSection(.pinned) == indexPath.section case let .status(id: id, state: state, pinned: pinned):
cell.updateUI(statusID: item.id, state: item.state) let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
cell.delegate = self
return cell cell.showPinned = pinned
cell.updateUI(statusID: id, state: state)
return cell
}
} }
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) { override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
@ -94,7 +97,7 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
self.mastodonController.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
DispatchQueue.main.async { DispatchQueue.main.async {
var snapshot = self.dataSource.snapshot() var snapshot = self.dataSource.snapshot()
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown, pinned: false) }, toSection: .statuses) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, pinned: false) }, toSection: .statuses)
if self.kind == .statuses { if self.kind == .statuses {
self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion) self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion)
} else { } else {
@ -122,7 +125,7 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
DispatchQueue.main.async { DispatchQueue.main.async {
var snapshot = snapshot() var snapshot = snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .pinned)) snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .pinned))
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown, pinned: true) }, toSection: .pinned) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, pinned: true) }, toSection: .pinned)
completion(.success(snapshot)) completion(.success(snapshot))
} }
} }
@ -151,7 +154,7 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
self.mastodonController.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
var snapshot = currentSnapshot() var snapshot = currentSnapshot()
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown, pinned: false) }, toSection: .statuses) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, pinned: false) }, toSection: .statuses)
completion(.success(snapshot)) completion(.success(snapshot))
} }
} }
@ -180,7 +183,7 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
self.mastodonController.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
var snapshot = currentSnapshot() var snapshot = currentSnapshot()
let items = statuses.map { Item(id: $0.id, state: .unknown, pinned: false) } let items = statuses.map { Item.status(id: $0.id, state: .unknown, pinned: false) }
if let first = snapshot.itemIdentifiers(inSection: .statuses).first { if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
snapshot.insertItems(items, beforeItem: first) snapshot.insertItems(items, beforeItem: first)
} else { } else {
@ -239,22 +242,22 @@ extension ProfileStatusesViewController {
} }
extension ProfileStatusesViewController { extension ProfileStatusesViewController {
enum Section: CaseIterable { enum Section: DiffableTimelineLikeSection {
case loadingIndicator
case pinned case pinned
case statuses case statuses
} }
struct Item: Hashable { enum Item: DiffableTimelineLikeItem {
let id: String case loadingIndicator
let state: StatusState case status(id: String, state: StatusState, pinned: Bool)
let pinned: Bool
static func ==(lhs: Item, rhs: Item) -> Bool { var id: String? {
return lhs.id == rhs.id && lhs.pinned == rhs.pinned switch self {
} case .loadingIndicator:
return nil
func hash(into hasher: inout Hasher) { case .status(id: let id, state: _, pinned: _):
hasher.combine(id) return id
hasher.combine(pinned) }
} }
} }
} }

View File

@ -97,6 +97,9 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? { override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
switch item { switch item {
case .loadingIndicator:
return self.loadingIndicatorCell(indexPath: indexPath)
case let .status(id: id, state: state): case let .status(id: id, state: state):
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
@ -148,6 +151,9 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
self.mastodonController.persistentContainer.addAll(statuses: statuses) { self.mastodonController.persistentContainer.addAll(statuses: statuses) {
DispatchQueue.main.async { DispatchQueue.main.async {
var snapshot = self.dataSource.snapshot() var snapshot = self.dataSource.snapshot()
if snapshot.sectionIdentifiers.contains(.loadingIndicator) {
snapshot.deleteSections([.loadingIndicator])
}
snapshot.deleteSections([.statuses, .footer]) snapshot.deleteSections([.statuses, .footer])
snapshot.appendSections([.statuses, .footer]) snapshot.appendSections([.statuses, .footer])
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
@ -245,12 +251,14 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
} }
extension TimelineTableViewController { extension TimelineTableViewController {
enum Section: Hashable, CaseIterable { enum Section: DiffableTimelineLikeSection {
case loadingIndicator
case header case header
case statuses case statuses
case footer case footer
} }
enum Item: Hashable { enum Item: DiffableTimelineLikeItem {
case loadingIndicator
case status(id: String, state: StatusState) case status(id: String, state: StatusState)
case confirmLoadMore case confirmLoadMore
case publicTimelineDescription(local: Bool) case publicTimelineDescription(local: Bool)
@ -270,13 +278,15 @@ extension TimelineTableViewController {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
switch self { switch self {
case let .status(id: id, state: _): case .loadingIndicator:
hasher.combine(0) hasher.combine(0)
case let .status(id: id, state: _):
hasher.combine(1)
hasher.combine(id) hasher.combine(id)
case .confirmLoadMore: case .confirmLoadMore:
hasher.combine(1)
case let .publicTimelineDescription(local: local):
hasher.combine(2) hasher.combine(2)
case let .publicTimelineDescription(local: local):
hasher.combine(3)
hasher.combine(local) hasher.combine(local)
} }
} }

View File

@ -9,7 +9,14 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable, Item: Hashable>: EnhancedTableViewController, RefreshableViewController { protocol DiffableTimelineLikeSection: Hashable, CaseIterable {
static var loadingIndicator: Self { get }
}
protocol DiffableTimelineLikeItem: Hashable {
static var loadingIndicator: Self { get }
}
class DiffableTimelineLikeTableViewController<Section: DiffableTimelineLikeSection, Item: DiffableTimelineLikeItem>: EnhancedTableViewController, RefreshableViewController {
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item> typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
typealias LoadResult = Result<Snapshot, LoadError> typealias LoadResult = Result<Snapshot, LoadError>
@ -40,6 +47,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140
tableView.register(LoadingTableViewCell.self, forCellReuseIdentifier: "loadingCell")
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
self.refreshControl = UIRefreshControl() self.refreshControl = UIRefreshControl()
@ -104,15 +112,34 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
dataSource.apply(snapshot, animatingDifferences: false) dataSource.apply(snapshot, animatingDifferences: false)
} }
private func showLoadingIndicatorDelayed() -> DispatchWorkItem {
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
var snapshot = self.dataSource.snapshot()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
self.dataSource.apply(snapshot, animatingDifferences: false)
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250), execute: workItem)
return workItem
}
private func loadInitial() { private func loadInitial() {
guard state == .unloaded else { return } guard state == .unloaded else { return }
// set loaded immediately so we don't trigger another request while the current one is running // set loaded immediately so we don't trigger another request while the current one is running
state = .loadingInitial state = .loadingInitial
let showIndicator = showLoadingIndicatorDelayed()
loadInitialItems() { result in loadInitialItems() { result in
DispatchQueue.main.async { DispatchQueue.main.async {
showIndicator.cancel()
switch result { switch result {
case let .success(snapshot): case .success(var snapshot):
if snapshot.sectionIdentifiers.contains(.loadingIndicator) {
snapshot.deleteSections([.loadingIndicator])
}
self.dataSource.apply(snapshot, animatingDifferences: false) self.dataSource.apply(snapshot, animatingDifferences: false)
self.state = .loaded self.state = .loaded
@ -137,25 +164,31 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
} }
func loadOlder() { func loadOlder() {
guard state != .loadingOlder else { return } guard state == .loaded else { return }
state = .loadingOlder state = .loadingOlder
let showIndicator = showLoadingIndicatorDelayed()
loadOlderItems(currentSnapshot: dataSource.snapshot) { result in loadOlderItems(currentSnapshot: dataSource.snapshot) { result in
DispatchQueue.main.async { DispatchQueue.main.async {
self.state = .loaded self.state = .loaded
showIndicator.cancel()
switch result { switch result {
case let .success(snapshot): case .success(var snapshot):
if snapshot.sectionIdentifiers.contains(.loadingIndicator) {
snapshot.deleteSections([.loadingIndicator])
}
self.dataSource.apply(snapshot, animatingDifferences: false) self.dataSource.apply(snapshot, animatingDifferences: false)
case let .failure(.client(error)): case let .failure(.client(error)):
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast) in let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.loadOlder() self?.loadOlder()
} }
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
default: default:
break break
} }
@ -263,6 +296,12 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
// MARK: - Subclass Methods // MARK: - Subclass Methods
func loadingIndicatorCell(indexPath: IndexPath) -> UITableViewCell? {
let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingTableViewCell
cell.indicator.startAnimating()
return cell
}
func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? { func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
fatalError("cellProvider(_:_:_:) must be implemented by subclasses") fatalError("cellProvider(_:_:_:) must be implemented by subclasses")
} }

View File

@ -0,0 +1,29 @@
//
// LoadingTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 9/12/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
class LoadingTableViewCell: UITableViewCell {
let indicator = UIActivityIndicatorView(style: .medium)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
indicator.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(indicator)
NSLayoutConstraint.activate([
indicator.centerXAnchor.constraint(equalTo: centerXAnchor),
indicator.topAnchor.constraint(equalTo: topAnchor),
indicator.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}