Convert the timeline to use diffable datasources

This commit is contained in:
Maurice Parker 2019-08-30 14:17:05 -05:00
parent 3baca1d7c0
commit 07ca61f7cf
4 changed files with 109 additions and 83 deletions

View File

@ -124,6 +124,7 @@
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; }; 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; };
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.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 */; }; 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
@ -1157,6 +1159,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
51C4526E2265091600C03939 /* MasterTimelineViewController.swift */, 51C4526E2265091600C03939 /* MasterTimelineViewController.swift */,
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */,
51C4526F2265091600C03939 /* Cell */, 51C4526F2265091600C03939 /* Cell */,
); );
path = MasterTimeline; path = MasterTimeline;
@ -2479,6 +2482,7 @@
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
51C45259226508D300C03939 /* AppDefaults.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */, 51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */,
51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */, 51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@ -468,11 +468,17 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
animatingChanges = false animatingChanges = false
} }
func indexForArticleID(_ articleID: String?) -> Int? {
guard let articleID = articleID else { return nil }
updateArticleRowMapIfNeeded()
return articleRowMap[articleID]
}
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet { func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet() var indexes = IndexSet()
articleIDs.forEach { (articleID) in articleIDs.forEach { (articleID) in
guard let oneIndex = row(for: articleID) else { guard let oneIndex = indexForArticleID(articleID) else {
return return
} }
if oneIndex != NSNotFound { if oneIndex != NSNotFound {
@ -482,7 +488,7 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexes return indexes
} }
func selectFeed(_ indexPath: IndexPath) { func selectFeed(_ indexPath: IndexPath) {
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count > 0 { if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count > 0 {
currentMasterIndexPath = indexPath currentMasterIndexPath = indexPath
@ -885,17 +891,12 @@ private extension AppCoordinator {
if articles != sortedArticles { if articles != sortedArticles {
let article = currentArticle let article = currentArticle
articles = sortedArticles 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) currentArticleIndexPath = IndexPath(row: index, section: 0)
} }
} }
} }
func row(for articleID: String) -> Int? {
updateArticleRowMapIfNeeded()
return articleRowMap[articleID]
}
func updateArticleRowMap() { func updateArticleRowMap() {
var rowMap = [String: Int]() var rowMap = [String: Int]()
var index = 0 var index = 0

View File

@ -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
}
}

View File

@ -18,6 +18,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem! @IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
@IBOutlet weak var firstUnreadButton: UIBarButtonItem! @IBOutlet weak var firstUnreadButton: UIBarButtonItem!
private lazy var dataSource = makeDataSource()
weak var coordinator: AppCoordinator! weak var coordinator: AppCoordinator!
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
@ -28,7 +29,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
tableView.dataSource = dataSource
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
@ -44,6 +46,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
numberOfTextLines = AppDefaults.timelineNumberOfLines numberOfTextLines = AppDefaults.timelineNumberOfLines
resetEstimatedRowHeight() resetEstimatedRowHeight()
applyChanges(animate: false)
resetUI() resetUI()
} }
@ -71,8 +74,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
appDelegate.authorAvatarDownloader.resetCache() appDelegate.authorAvatarDownloader.resetCache()
appDelegate.feedIconDownloader.resetCache() appDelegate.feedIconDownloader.resetCache()
appDelegate.faviconDownloader.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() { func reloadArticles() {
performBlockAndRestoreSelection { applyChanges(animate: true) { [weak self] in
tableView.reloadData() self?.updateArticleSelection()
} }
} }
@ -131,14 +136,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// MARK: - Table view // 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? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let article = coordinator.articles[indexPath.row] 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) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
coordinator.selectArticle(indexPath) coordinator.selectArticle(indexPath)
} }
@ -279,15 +269,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else { guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
return return
} }
tableView.indexPathsForVisibleRows?.forEach { indexPath in
performBlockAndRestoreSelection { guard let article = coordinator.articles.articleAtRow(indexPath.row) else {
tableView.indexPathsForVisibleRows?.forEach { indexPath in return
guard let article = coordinator.articles.articleAtRow(indexPath.row) else { }
return if article.feed == feed, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
} cell.setAvatarImage(image)
if article.feed == feed, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
cell.setAvatarImage(image)
}
} }
} }
} }
@ -296,16 +283,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
guard coordinator.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { guard coordinator.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
return return
} }
tableView.indexPathsForVisibleRows?.forEach { indexPath in
performBlockAndRestoreSelection { guard let article = coordinator.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
tableView.indexPathsForVisibleRows?.forEach { indexPath in return
guard let article = coordinator.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else { }
return for author in authors {
} if author.avatarURL == avatarURL, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
for author in authors { cell.setAvatarImage(image)
if author.avatarURL == avatarURL, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
cell.setAvatarImage(image)
}
} }
} }
} }
@ -315,18 +299,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
guard coordinator.showAvatars, let faviconURL = note.userInfo?["faviconURL"] as? String else { guard coordinator.showAvatars, let faviconURL = note.userInfo?["faviconURL"] as? String else {
return return
} }
tableView.indexPathsForVisibleRows?.forEach { indexPath in
performBlockAndRestoreSelection { guard let article = coordinator.articles.articleAtRow(indexPath.row), let articleFaviconURL = article.feed?.faviconURL else {
tableView.indexPathsForVisibleRows?.forEach { indexPath in return
}
guard let article = coordinator.articles.articleAtRow(indexPath.row), let articleFaviconURL = article.feed?.faviconURL else { if faviconURL == articleFaviconURL, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
return cell.setAvatarImage(image)
} return
if faviconURL == articleFaviconURL, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
cell.setAvatarImage(image)
return
}
} }
} }
} }
@ -335,12 +314,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
if numberOfTextLines != AppDefaults.timelineNumberOfLines { if numberOfTextLines != AppDefaults.timelineNumberOfLines {
numberOfTextLines = AppDefaults.timelineNumberOfLines numberOfTextLines = AppDefaults.timelineNumberOfLines
resetEstimatedRowHeight() resetEstimatedRowHeight()
tableView.reloadData() reloadAllVisibleCells()
} }
} }
@objc func contentSizeCategoryDidChange(_ note: Notification) { @objc func contentSizeCategoryDidChange(_ note: Notification) {
tableView.reloadData() reloadAllVisibleCells()
} }
@objc func progressDidChange(_ note: Notification) { @objc func progressDidChange(_ note: Notification) {
@ -349,12 +328,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// MARK: Reloading // MARK: Reloading
@objc func reloadAllVisibleCells() { private func reloadAllVisibleCells() {
tableView.beginUpdates() let visibleArticles = tableView.indexPathsForVisibleRows!.map { return coordinator.articles[$0.row] }
performBlockAndRestoreSelection { reloadCells(visibleArticles)
tableView.reloadRows(at: tableView.indexPathsForVisibleRows!, with: .none)
}
tableView.endUpdates()
} }
private func reloadVisibleCells(for articles: [Article]) { private func reloadVisibleCells(for articles: [Article]) {
@ -374,13 +350,22 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
private func reloadVisibleCells(for indexes: IndexSet) { private func reloadVisibleCells(for indexes: IndexSet) {
performBlockAndRestoreSelection { let reloadArticles: [Article] = tableView.indexPathsForVisibleRows!.compactMap { indexPath in
tableView.indexPathsForVisibleRows?.forEach { indexPath in if indexes.contains(indexPath.row) {
if indexes.contains(indexPath.row) { return coordinator.articles[indexPath.row]
tableView.reloadRows(at: [indexPath], with: .none) } 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()
}
} }
// MARK: Cell Configuring // MARK: Cell Configuring
@ -444,8 +429,27 @@ private extension MasterTimelineViewController {
navigationController?.updateAccountRefreshProgressIndicator() navigationController?.updateAccountRefreshProgressIndicator()
} }
} }
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 configureTimelineCell(_ cell: MasterTimelineTableViewCell, article: Article) { 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 avatar = avatarFor(article)
let featuredImage = featuredImageFor(article) let featuredImage = featuredImageFor(article)
@ -470,19 +474,11 @@ private extension MasterTimelineViewController {
return nil return nil
} }
func queueReloadVisableCells() { func restoreSelectionIfNecessary() {
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
}
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
guard traitCollection.userInterfaceIdiom == .pad else { guard traitCollection.userInterfaceIdiom == .pad else {
block()
return return
} }
if let articleID = coordinator.currentArticle?.articleID, let index = coordinator.indexesForArticleIDs(Set([articleID])).first {
let articleID = coordinator.currentArticle?.articleID
block()
if let articleID = articleID, let index = coordinator.indexesForArticleIDs(Set([articleID])).first {
let indexPath = IndexPath(row: index, section: 0) let indexPath = IndexPath(row: index, section: 0)
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
} }