diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 51a138d08..749281e4b 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -244,6 +244,8 @@ 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 */; }; + 51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; }; + 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */; }; 51E36E71239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */; }; 51E36E8C239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; @@ -1379,6 +1381,8 @@ 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; + 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = ""; }; + 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = ""; }; 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedSelectFolderTableViewCell.swift; sourceTree = ""; }; 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddWebFeedSelectFolderTableViewCell.xib; sourceTree = ""; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; @@ -1947,11 +1951,13 @@ isa = PBXGroup; children = ( 51C45264226508F600C03939 /* MasterFeedViewController.swift */, - 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */, 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */, 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */, + 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */, + 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */, 51CE1C0A23622006005548FC /* RefreshProgressView.swift */, 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */, + 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */, 51C45260226508F600C03939 /* Cell */, ); path = MasterFeed; @@ -4004,6 +4010,7 @@ 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, + 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, @@ -4016,6 +4023,7 @@ FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, + 51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */, diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift index feec33e2e..150644fc5 100644 --- a/iOS/MasterFeed/MasterFeedDataSource.swift +++ b/iOS/MasterFeed/MasterFeedDataSource.swift @@ -7,7 +7,6 @@ // import UIKit -import RSCore import RSTree import Account diff --git a/iOS/MasterFeed/MasterFeedDataSourceOperation.swift b/iOS/MasterFeed/MasterFeedDataSourceOperation.swift new file mode 100644 index 000000000..f96310346 --- /dev/null +++ b/iOS/MasterFeed/MasterFeedDataSourceOperation.swift @@ -0,0 +1,39 @@ +// +// MasterFeedDataSourceOperation.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 2/23/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import UIKit +import RSCore +import RSTree + +class MasterFeedDataSourceOperation: MainThreadOperation { + + // MainThreadOperation + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? = "MasterFeedDataSourceOperation" + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private var dataSource: UITableViewDiffableDataSource + private var snapshot: NSDiffableDataSourceSnapshot + private var animating: Bool + + init(dataSource: UITableViewDiffableDataSource, snapshot: NSDiffableDataSourceSnapshot, animating: Bool) { + self.dataSource = dataSource + self.snapshot = snapshot + self.animating = animating + } + + func run() { + dataSource.apply(snapshot, animatingDifferences: animating) { [weak self] in + guard let self = self else { return } + self.operationDelegate?.operationDidComplete(self) + } + } + +} diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 078d9a617..86382a44f 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -17,8 +17,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @IBOutlet weak var filterButton: UIBarButtonItem! private var refreshProgressView: RefreshProgressView? @IBOutlet weak var addNewItemButton: UIBarButtonItem! - + + private let operationQueue = MainThreadOperationQueue() lazy var dataSource = makeDataSource() + var undoableCommands = [UndoableCommand]() weak var coordinator: SceneCoordinator! @@ -27,7 +29,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return keyboardManager.keyCommands } - var restoreSelection = false override var canBecomeFirstResponder: Bool { return true } @@ -73,17 +74,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { updateUI() super.viewWillAppear(animated) } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - // We have to delay the selection of the Feed until the subviews have been layed out - // so that the visible area is correct and we scroll correctly. - if restoreSelection { - restoreSelectionIfNecessary(adjustScroll: true) - restoreSelection = false - } - } // MARK: Notifications @@ -144,10 +134,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } - @objc func webFeedMetadataDidChange(_ note: Notification) { - reloadAllVisibleCells() - } - @objc func contentSizeCategoryDidChange(_ note: Notification) { resetEstimatedRowHeight() applyChanges(animated: false) @@ -517,24 +503,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } func updateFeedSelection(animations: Animations) { - if dataSource.snapshot().numberOfItems > 0 { - if let indexPath = coordinator.currentFeedIndexPath { - if tableView.indexPathForSelectedRow != indexPath { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations) - } - } else { - if animations.contains(.select) { - // This nasty bit of duct tape is because there is something, somewhere - // interrupting the deselection animation, which will leave the row selected. - // This seems to get it far enough away the problem that it always works. - DispatchQueue.main.async { - self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - } - } else { - self.tableView.selectRow(at: nil, animated: false, scrollPosition: .none) - } - } - } + operationQueue.add(UpdateSelectionOperation(coordinator: coordinator, dataSource: dataSource, tableView: tableView, animations: animations)) } func reloadFeeds(initialLoad: Bool, completion: (() -> Void)? = nil) { @@ -646,7 +615,7 @@ private extension MasterFeedViewController { func reloadNode(_ node: Node) { var snapshot = dataSource.snapshot() snapshot.reloadItems([node]) - dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in + queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in self?.restoreSelectionIfNecessary(adjustScroll: false) } } @@ -661,13 +630,22 @@ private extension MasterFeedViewController { snapshot.appendItems(shadowTableNodes, toSection: sectionNode) } - dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in + queueApply(snapshot: snapshot, animatingDifferences: animated) { [weak self] in self?.restoreSelectionIfNecessary(adjustScroll: adjustScroll) completion?() } } + + func queueApply(snapshot: NSDiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { + let operation = MasterFeedDataSourceOperation(dataSource: dataSource, snapshot: snapshot, animating: animatingDifferences) + operation.completionBlock = { _ in + completion?() + } + operationQueue.add(operation) + } - func makeDataSource() -> UITableViewDiffableDataSource { + + func makeDataSource() -> MasterFeedDataSource { let dataSource = MasterFeedDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell self?.configure(cell, node) @@ -774,7 +752,7 @@ private extension MasterFeedViewController { private func reloadCells(_ nodes: [Node], completion: (() -> Void)? = nil) { var snapshot = dataSource.snapshot() snapshot.reloadItems(nodes) - dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in + queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in self?.restoreSelectionIfNecessary(adjustScroll: false) completion?() } diff --git a/iOS/MasterFeed/UpdateSelectionOperation.swift b/iOS/MasterFeed/UpdateSelectionOperation.swift new file mode 100644 index 000000000..7a4820586 --- /dev/null +++ b/iOS/MasterFeed/UpdateSelectionOperation.swift @@ -0,0 +1,51 @@ +// +// UpdateSelectionOperation.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 2/22/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import UIKit +import RSCore + +class UpdateSelectionOperation: MainThreadOperation { + + // MainThreadOperation + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? = "UpdateSelectionOperation" + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private var coordinator: SceneCoordinator + private var dataSource: MasterFeedDataSource + private var tableView: UITableView + private var animations: Animations + + init(coordinator: SceneCoordinator, dataSource: MasterFeedDataSource, tableView: UITableView, animations: Animations) { + self.coordinator = coordinator + self.dataSource = dataSource + self.tableView = tableView + self.animations = animations + } + + func run() { + if dataSource.snapshot().numberOfItems > 0 { + if let indexPath = coordinator.currentFeedIndexPath { + CATransaction.begin() + CATransaction.setCompletionBlock { + self.operationDelegate?.operationDidComplete(self) + } + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations) + CATransaction.commit() + } else { + tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none) + self.operationDelegate?.operationDidComplete(self) + } + } else { + self.operationDelegate?.operationDidComplete(self) + } + } + +} diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a85554c28..feb62d86b 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1738,7 +1738,9 @@ private extension SceneCoordinator { } @objc func fetchAndMergeArticlesAsync() { - fetchAndMergeArticlesAsync(animated: true, completion: nil) + fetchAndMergeArticlesAsync(animated: true) { + self.masterTimelineViewController?.reinitializeArticles(resetScroll: false) + } } func fetchAndMergeArticlesAsync(animated: Bool = true, completion: (() -> Void)? = nil) { @@ -2002,7 +2004,6 @@ private extension SceneCoordinator { } treeControllerDelegate.addFilterException(feedIdentifier) - masterFeedViewController.restoreSelection = true switch feedIdentifier { @@ -2087,8 +2088,6 @@ private extension SceneCoordinator { return false } - masterFeedViewController.restoreSelection = true - switch feedIdentifier { case .smartFeed: