forked from shadowfacts/Tusker
Add loading indicator to DiffableTimelineLikeTableViewController
This commit is contained in:
parent
8b78a5e7ad
commit
bbfb3b0a7a
|
@ -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 */,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -60,15 +60,18 @@ 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? {
|
||||||
|
switch item {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return self.loadingIndicatorCell(indexPath: indexPath)
|
||||||
|
|
||||||
|
case let .status(id: id, state: state, pinned: pinned):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||||
|
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
// todo: dataSource.sectionIdentifier is only available on iOS 15
|
cell.showPinned = pinned
|
||||||
cell.showPinned = dataSource.snapshot().indexOfSection(.pinned) == indexPath.section
|
cell.updateUI(statusID: id, state: state)
|
||||||
cell.updateUI(statusID: item.id, state: item.state)
|
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
guard accountID != nil else {
|
guard accountID != nil else {
|
||||||
|
@ -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
|
||||||
|
case .status(id: let id, state: _, pinned: _):
|
||||||
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
hasher.combine(pinned)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,16 +164,22 @@ 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)):
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue