Convert the timeline to use diffable datasources
This commit is contained in:
parent
3baca1d7c0
commit
07ca61f7cf
|
@ -124,6 +124,7 @@
|
|||
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
|
||||
51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; };
|
||||
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
|
||||
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; };
|
||||
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
|
||||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
|
||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; };
|
||||
|
@ -735,6 +736,7 @@
|
|||
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
|
||||
51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
|
||||
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
||||
51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
|
||||
51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||
51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||
|
@ -1157,6 +1159,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
51C4526E2265091600C03939 /* MasterTimelineViewController.swift */,
|
||||
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */,
|
||||
51C4526F2265091600C03939 /* Cell */,
|
||||
);
|
||||
path = MasterTimeline;
|
||||
|
@ -2479,6 +2482,7 @@
|
|||
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
|
||||
51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
|
||||
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
|
||||
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */,
|
||||
51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -468,11 +468,17 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||
animatingChanges = false
|
||||
}
|
||||
|
||||
func indexForArticleID(_ articleID: String?) -> Int? {
|
||||
guard let articleID = articleID else { return nil }
|
||||
updateArticleRowMapIfNeeded()
|
||||
return articleRowMap[articleID]
|
||||
}
|
||||
|
||||
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
|
||||
var indexes = IndexSet()
|
||||
|
||||
articleIDs.forEach { (articleID) in
|
||||
guard let oneIndex = row(for: articleID) else {
|
||||
guard let oneIndex = indexForArticleID(articleID) else {
|
||||
return
|
||||
}
|
||||
if oneIndex != NSNotFound {
|
||||
|
@ -885,17 +891,12 @@ private extension AppCoordinator {
|
|||
if articles != sortedArticles {
|
||||
let article = currentArticle
|
||||
articles = sortedArticles
|
||||
if let articleID = article?.articleID, let index = row(for: articleID) {
|
||||
if let articleID = article?.articleID, let index = indexForArticleID(articleID) {
|
||||
currentArticleIndexPath = IndexPath(row: index, section: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func row(for articleID: String) -> Int? {
|
||||
updateArticleRowMapIfNeeded()
|
||||
return articleRowMap[articleID]
|
||||
}
|
||||
|
||||
func updateArticleRowMap() {
|
||||
var rowMap = [String: Int]()
|
||||
var index = 0
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// MasterTimelineDataSource.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 8/30/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class MasterTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {
|
||||
|
||||
private var coordinator: AppCoordinator!
|
||||
|
||||
init(coordinator: AppCoordinator, tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider) {
|
||||
super.init(tableView: tableView, cellProvider: cellProvider)
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
|
||||
@IBOutlet weak var firstUnreadButton: UIBarButtonItem!
|
||||
|
||||
private lazy var dataSource = makeDataSource()
|
||||
weak var coordinator: AppCoordinator!
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
|
@ -28,6 +29,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
override func viewDidLoad() {
|
||||
|
||||
super.viewDidLoad()
|
||||
tableView.dataSource = dataSource
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
|
@ -44,6 +46,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
||||
resetEstimatedRowHeight()
|
||||
|
||||
applyChanges(animate: false)
|
||||
resetUI()
|
||||
|
||||
}
|
||||
|
@ -71,8 +74,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
appDelegate.authorAvatarDownloader.resetCache()
|
||||
appDelegate.feedIconDownloader.resetCache()
|
||||
appDelegate.faviconDownloader.resetCache()
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.reloadData()
|
||||
|
||||
// traitCollectionDidChange will get called on a background thread
|
||||
DispatchQueue.main.async {
|
||||
self.reloadAllVisibleCells()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,8 +120,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
func reloadArticles() {
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.reloadData()
|
||||
applyChanges(animate: true) { [weak self] in
|
||||
self?.updateArticleSelection()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,14 +136,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
// MARK: - Table view
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return coordinator.articles.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
let article = coordinator.articles[indexPath.row]
|
||||
|
@ -251,13 +248,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
|
||||
let article = coordinator.articles[indexPath.row]
|
||||
configureTimelineCell(cell, article: article)
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
coordinator.selectArticle(indexPath)
|
||||
}
|
||||
|
@ -279,8 +269,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
guard let article = coordinator.articles.articleAtRow(indexPath.row) else {
|
||||
return
|
||||
|
@ -290,14 +278,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
guard coordinator.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
guard let article = coordinator.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
return
|
||||
|
@ -309,16 +294,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
guard coordinator.showAvatars, let faviconURL = note.userInfo?["faviconURL"] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = coordinator.articles.articleAtRow(indexPath.row), let articleFaviconURL = article.feed?.faviconURL else {
|
||||
return
|
||||
}
|
||||
|
@ -326,8 +307,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
cell.setAvatarImage(image)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,12 +314,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
if numberOfTextLines != AppDefaults.timelineNumberOfLines {
|
||||
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
||||
resetEstimatedRowHeight()
|
||||
tableView.reloadData()
|
||||
reloadAllVisibleCells()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
tableView.reloadData()
|
||||
reloadAllVisibleCells()
|
||||
}
|
||||
|
||||
@objc func progressDidChange(_ note: Notification) {
|
||||
|
@ -349,12 +328,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
// MARK: Reloading
|
||||
|
||||
@objc func reloadAllVisibleCells() {
|
||||
tableView.beginUpdates()
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.reloadRows(at: tableView.indexPathsForVisibleRows!, with: .none)
|
||||
}
|
||||
tableView.endUpdates()
|
||||
private func reloadAllVisibleCells() {
|
||||
let visibleArticles = tableView.indexPathsForVisibleRows!.map { return coordinator.articles[$0.row] }
|
||||
reloadCells(visibleArticles)
|
||||
}
|
||||
|
||||
private func reloadVisibleCells(for articles: [Article]) {
|
||||
|
@ -374,12 +350,21 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
private func reloadVisibleCells(for indexes: IndexSet) {
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
let reloadArticles: [Article] = tableView.indexPathsForVisibleRows!.compactMap { indexPath in
|
||||
if indexes.contains(indexPath.row) {
|
||||
tableView.reloadRows(at: [indexPath], with: .none)
|
||||
return coordinator.articles[indexPath.row]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
reloadCells(reloadArticles)
|
||||
}
|
||||
|
||||
private func reloadCells(_ articles: [Article]) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems(articles)
|
||||
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,7 +430,26 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func configureTimelineCell(_ cell: MasterTimelineTableViewCell, article: Article) {
|
||||
func applyChanges(animate: Bool, completion: (() -> Void)? = nil) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(coordinator.articles, toSection: 0)
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: animate) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> {
|
||||
return MasterTimelineDataSource(coordinator: coordinator, tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
|
||||
self?.configure(cell, article: article)
|
||||
return cell
|
||||
})
|
||||
}
|
||||
|
||||
func configure(_ cell: MasterTimelineTableViewCell, article: Article) {
|
||||
|
||||
let avatar = avatarFor(article)
|
||||
let featuredImage = featuredImageFor(article)
|
||||
|
@ -470,19 +474,11 @@ private extension MasterTimelineViewController {
|
|||
return nil
|
||||
}
|
||||
|
||||
func queueReloadVisableCells() {
|
||||
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
|
||||
}
|
||||
|
||||
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
||||
func restoreSelectionIfNecessary() {
|
||||
guard traitCollection.userInterfaceIdiom == .pad else {
|
||||
block()
|
||||
return
|
||||
}
|
||||
|
||||
let articleID = coordinator.currentArticle?.articleID
|
||||
block()
|
||||
if let articleID = articleID, let index = coordinator.indexesForArticleIDs(Set([articleID])).first {
|
||||
if let articleID = coordinator.currentArticle?.articleID, let index = coordinator.indexesForArticleIDs(Set([articleID])).first {
|
||||
let indexPath = IndexPath(row: index, section: 0)
|
||||
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue