From 48e856fc043ef862925ca06af49085328a02511d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 23 Feb 2020 10:57:20 -0800 Subject: [PATCH 01/51] Serialize access to the UITableView for scrolling and diffable datasource updates. Issue #1806 --- NetNewsWire.xcodeproj/project.pbxproj | 10 +++- iOS/MasterFeed/MasterFeedDataSource.swift | 1 - .../MasterFeedDataSourceOperation.swift | 39 +++++++++++++ iOS/MasterFeed/MasterFeedViewController.swift | 56 ++++++------------- iOS/MasterFeed/UpdateSelectionOperation.swift | 51 +++++++++++++++++ iOS/SceneCoordinator.swift | 7 +-- 6 files changed, 119 insertions(+), 45 deletions(-) create mode 100644 iOS/MasterFeed/MasterFeedDataSourceOperation.swift create mode 100644 iOS/MasterFeed/UpdateSelectionOperation.swift 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: From afadadff69269a69279986a7f9974a0fd4c4b80e Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 23 Feb 2020 17:12:02 -0800 Subject: [PATCH 02/51] Restore timeline selection when coming back into foreground. (Regression) --- iOS/SceneCoordinator.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index feb62d86b..254e63ba9 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1740,6 +1740,7 @@ private extension SceneCoordinator { @objc func fetchAndMergeArticlesAsync() { fetchAndMergeArticlesAsync(animated: true) { self.masterTimelineViewController?.reinitializeArticles(resetScroll: false) + self.masterTimelineViewController?.restoreSelectionIfNecessary(adjustScroll: false) } } From 23b8af46349f2108562006ad3c790fa7875729a2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 24 Feb 2020 08:07:14 -0800 Subject: [PATCH 03/51] Don't end the operation until the deselection animation has completed. (Regression) --- iOS/MasterFeed/UpdateSelectionOperation.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/iOS/MasterFeed/UpdateSelectionOperation.swift b/iOS/MasterFeed/UpdateSelectionOperation.swift index 7a4820586..287e093db 100644 --- a/iOS/MasterFeed/UpdateSelectionOperation.swift +++ b/iOS/MasterFeed/UpdateSelectionOperation.swift @@ -40,8 +40,17 @@ class UpdateSelectionOperation: MainThreadOperation { tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations) CATransaction.commit() } else { - tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none) - self.operationDelegate?.operationDidComplete(self) + if animations.contains(.select) { + CATransaction.begin() + CATransaction.setCompletionBlock { + self.operationDelegate?.operationDidComplete(self) + } + tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + CATransaction.commit() + } else { + tableView.selectRow(at: nil, animated: false, scrollPosition: .none) + self.operationDelegate?.operationDidComplete(self) + } } } else { self.operationDelegate?.operationDidComplete(self) From d222a9f6174e501de2f5a7b689c5d4e89ee76040 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 24 Feb 2020 08:42:19 -0800 Subject: [PATCH 04/51] Change to not force unwrap the descriptor. Issue #1818 --- iOS/UIKit Extensions/UIFont-Extensions.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/iOS/UIKit Extensions/UIFont-Extensions.swift b/iOS/UIKit Extensions/UIFont-Extensions.swift index 64620b9d1..d250066b8 100644 --- a/iOS/UIKit Extensions/UIFont-Extensions.swift +++ b/iOS/UIKit Extensions/UIFont-Extensions.swift @@ -11,8 +11,11 @@ import UIKit extension UIFont { func withTraits(traits:UIFontDescriptor.SymbolicTraits) -> UIFont { - let descriptor = fontDescriptor.withSymbolicTraits(traits) - return UIFont(descriptor: descriptor!, size: 0) //size 0 means keep the size as it is + if let descriptor = fontDescriptor.withSymbolicTraits(traits) { + return UIFont(descriptor: descriptor, size: 0) //size 0 means keep the size as it is + } else { + return self + } } func bold() -> UIFont { From a4bbf659442d1d97ea3709a01aa61e46ce169c1c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 25 Feb 2020 10:42:56 -0800 Subject: [PATCH 05/51] Coalesce unread count backing store rebuilds to prevent feeds list flickering while syncing and the feeds filter is engaged. (Regression) --- iOS/SceneCoordinator.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 254e63ba9..5df9e65ab 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -61,6 +61,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private var wasRootSplitViewControllerCollapsed = false private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) + private let rebuildBackingStoresWithMergeQueue = CoalescingQueue(name: "Rebuild The Backing Stores by Merging", interval: 0.5) private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() @@ -445,9 +446,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return } - addShadowTableToFilterExceptions() - rebuildBackingStores() - treeControllerDelegate.resetFilterExceptions() + rebuildBackingStoresWithMergeQueue.add(self, #selector(rebuildBackingStoresWithMerge)) } @objc func statusesDidChange(_ note: Notification) { @@ -569,6 +568,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func suspend() { fetchAndMergeArticlesQueue.performCallsImmediately() + rebuildBackingStoresWithMergeQueue.performCallsImmediately() fetchRequestQueue.cancelAllRequests() } @@ -1362,6 +1362,12 @@ private extension SceneCoordinator { } } + @objc func rebuildBackingStoresWithMerge() { + addShadowTableToFilterExceptions() + rebuildBackingStores() + treeControllerDelegate.resetFilterExceptions() + } + func rebuildShadowTable() { shadowTable = [[Node]]() From 5a5abb0b8772702102df7f8ed96e4b6b2c19ea49 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 25 Feb 2020 15:10:51 -0800 Subject: [PATCH 06/51] Ensure that the dom is fully loaded on *all* web views before being made available to process JavaScript. Issue #1756 & Issue #1808 --- NetNewsWire.xcodeproj/project.pbxproj | 4 + iOS/Article/PreloadedWebView.swift | 81 +++++++++++++++++++ iOS/Article/WebViewController.swift | 8 +- iOS/Article/WebViewProvider.swift | 112 +++++--------------------- 4 files changed, 109 insertions(+), 96 deletions(-) create mode 100644 iOS/Article/PreloadedWebView.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 749281e4b..56562fec1 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -246,6 +246,7 @@ 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 */; }; + 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.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 */; }; @@ -1383,6 +1384,7 @@ 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 = ""; }; + 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.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 = ""; }; @@ -2015,6 +2017,7 @@ 518651D9235621840078E021 /* ImageTransition.swift */, 5142192923522B5500E07E2C /* ImageViewController.swift */, 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */, + 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */, 51AB8AB223B7F4C6008F147D /* WebViewController.swift */, 517630222336657E00E15FFF /* WebViewProvider.swift */, 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */, @@ -4009,6 +4012,7 @@ 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, + 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, diff --git a/iOS/Article/PreloadedWebView.swift b/iOS/Article/PreloadedWebView.swift new file mode 100644 index 000000000..1573b6a3d --- /dev/null +++ b/iOS/Article/PreloadedWebView.swift @@ -0,0 +1,81 @@ +// +// PreloadedWebView.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 2/25/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import WebKit + +class PreloadedWebView: WKWebView { + + private struct MessageName { + static let domContentLoaded = "domContentLoaded" + } + + private var isReady: Bool = false + private var readyCompletion: ((PreloadedWebView) -> Void)? + + init(articleIconSchemeHandler: ArticleIconSchemeHandler) { + let preferences = WKPreferences() + preferences.javaScriptCanOpenWindowsAutomatically = false + preferences.javaScriptEnabled = true + + let configuration = WKWebViewConfiguration() + configuration.preferences = preferences + configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") + configuration.allowsInlineMediaPlayback = true + configuration.mediaTypesRequiringUserActionForPlayback = .video + configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + + super.init(frame: .zero, configuration: configuration) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func preload() { + configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded) + loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL) + } + + func ready(completion: @escaping (PreloadedWebView) -> Void) { + if isReady { + completeRequest(completion: completion) + } else { + readyCompletion = completion + } + } + +} + +// MARK: WKScriptMessageHandler + +extension PreloadedWebView: WKScriptMessageHandler { + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == MessageName.domContentLoaded { + isReady = true + if let completion = readyCompletion { + completeRequest(completion: completion) + readyCompletion = nil + } + } + } + +} + +// MARK: Private + +private extension PreloadedWebView { + + func completeRequest(completion: @escaping (PreloadedWebView) -> Void) { + isReady = false + configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded) + completion(self) + } + +} diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 111a1e23f..528e863b8 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -29,8 +29,8 @@ class WebViewController: UIViewController { private var topShowBarsViewConstraint: NSLayoutConstraint! private var bottomShowBarsViewConstraint: NSLayoutConstraint! - private var webView: WKWebView? { - return view.subviews[0] as? WKWebView + private var webView: PreloadedWebView? { + return view.subviews[0] as? PreloadedWebView } private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self) @@ -450,7 +450,7 @@ private extension WebViewController { } - func recycleWebView(_ webView: WKWebView?) { + func recycleWebView(_ webView: PreloadedWebView?) { guard let webView = webView else { return } webView.removeFromSuperview() @@ -467,7 +467,7 @@ private extension WebViewController { coordinator.webViewProvider.enqueueWebView(webView) } - func renderPage(_ webView: WKWebView?) { + func renderPage(_ webView: PreloadedWebView?) { guard let webView = webView else { return } let style = ArticleStylesManager.shared.currentStyle diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift index 8fda2d206..e48b16764 100644 --- a/iOS/Article/WebViewProvider.swift +++ b/iOS/Article/WebViewProvider.swift @@ -11,21 +11,14 @@ import WebKit /// WKWebView has an awful behavior of a flash to white on first load when in dark mode. /// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle. -class WebViewProvider: NSObject, WKNavigationDelegate { +class WebViewProvider: NSObject { - private struct MessageName { - static let domContentLoaded = "domContentLoaded" - } - let articleIconSchemeHandler: ArticleIconSchemeHandler private let minimumQueueDepth = 3 private let maximumQueueDepth = 6 private var queue = UIView() - private var waitingForFirstLoad = true - private var waitingCompletionHandler: ((WKWebView) -> ())? - init(coordinator: SceneCoordinator, viewController: UIViewController) { articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator) super.init() @@ -47,104 +40,39 @@ class WebViewProvider: NSObject, WKNavigationDelegate { func flushQueue() { queue.subviews.forEach { $0.removeFromSuperview() } - waitingForFirstLoad = true } func replenishQueueIfNeeded() { while queue.subviews.count < minimumQueueDepth { - let webView = WKWebView(frame: .zero, configuration: buildConfiguration()) - enqueueWebView(webView) + enqueueWebView(PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)) } } - func dequeueWebView(completion: @escaping (WKWebView) -> ()) { - if waitingForFirstLoad { - waitingCompletionHandler = completion - } else { - completeRequest(completion: completion) + func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) { + if let webView = queue.subviews.last as? PreloadedWebView { + webView.ready { preloadedWebView in + preloadedWebView.removeFromSuperview() + self.replenishQueueIfNeeded() + completion(preloadedWebView) + } + return + } + + assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.") + + let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) + webView.ready { preloadedWebView in + self.replenishQueueIfNeeded() + completion(preloadedWebView) } } - func enqueueWebView(_ webView: WKWebView) { + func enqueueWebView(_ webView: PreloadedWebView) { guard queue.subviews.count < maximumQueueDepth else { return } - - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded) queue.insertSubview(webView, at: 0) - - webView.loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL) - } - - // MARK: WKNavigationDelegate - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if waitingForFirstLoad { - waitingForFirstLoad = false - if let completion = waitingCompletionHandler { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.completeRequest(completion: completion) - self.waitingCompletionHandler = nil - } - } - } + webView.preload() } } - -// MARK: WKScriptMessageHandler - -extension WebViewProvider: WKScriptMessageHandler { - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - switch message.name { - case MessageName.domContentLoaded: - if waitingForFirstLoad { - waitingForFirstLoad = false - if let completion = waitingCompletionHandler { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.completeRequest(completion: completion) - self.waitingCompletionHandler = nil - } - } - } - default: - return - } - } - -} - -// MARK: Private - -private extension WebViewProvider { - - func completeRequest(completion: @escaping (WKWebView) -> ()) { - if let webView = queue.subviews.last as? WKWebView { - webView.removeFromSuperview() - webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded) - replenishQueueIfNeeded() - completion(webView) - return - } - - assertionFailure("Creating WKWebView in \(#function); queue has run dry.") - let webView = WKWebView(frame: .zero) - completion(webView) - } - - func buildConfiguration() -> WKWebViewConfiguration { - let preferences = WKPreferences() - preferences.javaScriptCanOpenWindowsAutomatically = false - preferences.javaScriptEnabled = true - - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") - configuration.allowsInlineMediaPlayback = true - configuration.mediaTypesRequiringUserActionForPlayback = .video - configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) - - return configuration - } -} From 9e3f061fcb2e0a1c727a407e3ad200b2d8c21515 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 25 Feb 2020 18:06:02 -0800 Subject: [PATCH 07/51] Dispatch the page controller setting since it is unreliable. Issue #1756 & Issue #1808 --- iOS/Article/ArticleViewController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index b189719dc..eeac8f4cd 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -111,7 +111,10 @@ class ArticleViewController: UIViewController { controller.windowScrollY = state.windowScrollY } articleExtractorButton.buttonState = controller.articleExtractorButtonState - pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) + + DispatchQueue.main.async { + self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) + } updateUI() } From cd5e491f00a7c8ed7aa05c99c4b2f0d93f4e5a54 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 26 Feb 2020 16:02:36 -0800 Subject: [PATCH 08/51] Fix issue where full screen wouldn't come back after being engaged and moving to the timeline. (Regression) --- iOS/Article/ArticleViewController.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index eeac8f4cd..ff5328dd3 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -114,18 +114,14 @@ class ArticleViewController: UIViewController { DispatchQueue.main.async { self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) + if AppDefaults.articleFullscreenEnabled { + controller.hideBars() + } } updateUI() } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if AppDefaults.articleFullscreenEnabled { - currentWebViewController?.hideBars() - } - } - override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(true) coordinator.isArticleViewControllerPending = false From 3a0f57e4daea7bd3d48b66a2ea55b4f23ea3dfe0 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 29 Feb 2020 09:09:53 -0800 Subject: [PATCH 09/51] Fixed toggle color. Issue #1838 --- iOS/Settings/Settings.storyboard | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index ce2581b60..97235c536 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -19,7 +19,7 @@ - + @@ -281,13 +281,13 @@ - + - + @@ -168,7 +170,9 @@ - + + + @@ -191,7 +195,7 @@ - + @@ -201,7 +205,9 @@ - + + + @@ -249,7 +255,7 @@ - + @@ -259,9 +265,11 @@ + - + + @@ -280,7 +288,7 @@ - + @@ -298,7 +306,8 @@ - + + @@ -311,7 +320,7 @@ - + @@ -328,7 +337,7 @@ - + @@ -345,7 +354,7 @@ - + @@ -362,14 +371,14 @@ - + - + - + - + - + - +