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 */; };
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;

View File

@ -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 {
@ -482,7 +488,7 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexes
}
func selectFeed(_ indexPath: IndexPath) {
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count > 0 {
currentMasterIndexPath = indexPath
@ -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

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 firstUnreadButton: UIBarButtonItem!
private lazy var dataSource = makeDataSource()
weak var coordinator: AppCoordinator!
var undoableCommands = [UndoableCommand]()
@ -28,7 +29,8 @@ 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)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, 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,15 +269,12 @@ 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
}
if article.feed == feed, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
cell.setAvatarImage(image)
}
tableView.indexPathsForVisibleRows?.forEach { indexPath in
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)
}
}
}
@ -296,16 +283,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
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
}
for author in authors {
if author.avatarURL == avatarURL, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
cell.setAvatarImage(image)
}
tableView.indexPathsForVisibleRows?.forEach { indexPath in
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) {
cell.setAvatarImage(image)
}
}
}
@ -315,18 +299,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
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
}
if faviconURL == articleFaviconURL, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
cell.setAvatarImage(image)
return
}
tableView.indexPathsForVisibleRows?.forEach { indexPath in
guard let article = coordinator.articles.articleAtRow(indexPath.row), let articleFaviconURL = article.feed?.faviconURL else {
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 {
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,13 +350,22 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
private func reloadVisibleCells(for indexes: IndexSet) {
performBlockAndRestoreSelection {
tableView.indexPathsForVisibleRows?.forEach { indexPath in
if indexes.contains(indexPath.row) {
tableView.reloadRows(at: [indexPath], with: .none)
}
let reloadArticles: [Article] = tableView.indexPathsForVisibleRows!.compactMap { indexPath in
if indexes.contains(indexPath.row) {
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()
}
}
// MARK: Cell Configuring
@ -444,8 +429,27 @@ private extension MasterTimelineViewController {
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 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)
}