Restore article scroll position on iOS

This commit is contained in:
Maurice Parker 2021-09-13 01:11:23 -05:00
parent 87700325cf
commit 5001d82355
4 changed files with 67 additions and 27 deletions

View File

@ -23,5 +23,7 @@ struct UserInfoKey {
static let readArticlesFilterStateKeys = "readArticlesFilterStateKey" static let readArticlesFilterStateKeys = "readArticlesFilterStateKey"
static let readArticlesFilterStateValues = "readArticlesFilterStateValue" static let readArticlesFilterStateValues = "readArticlesFilterStateValue"
static let selectedFeedsState = "selectedFeedsState" static let selectedFeedsState = "selectedFeedsState"
static let isShowingExtractedArticle = "isShowingExtractedArticle"
static let articleWindowScrollY = "articleWindowScrollY"
} }

View File

@ -311,7 +311,11 @@ class ArticleViewController: UIViewController {
func openInAppBrowser() { func openInAppBrowser() {
currentWebViewController?.openInAppBrowser() currentWebViewController?.openInAppBrowser()
} }
func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) {
currentWebViewController?.setScrollPosition(isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
}
} }
// MARK: Find in Article // MARK: Find in Article

View File

@ -63,7 +63,8 @@ class WebViewController: UIViewController {
let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3)
var windowScrollY = 0 var windowScrollY = 0
private var restoreWindowScrollY: Int?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -129,6 +130,27 @@ class WebViewController: UIViewController {
} }
func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) {
if isShowingExtractedArticle {
switch articleExtractor?.state {
case .ready:
restoreWindowScrollY = articleWindowScrollY
startArticleExtractor()
case .complete:
windowScrollY = articleWindowScrollY
loadWebView()
case .processing:
restoreWindowScrollY = articleWindowScrollY
default:
restoreWindowScrollY = articleWindowScrollY
startArticleExtractor()
}
} else {
windowScrollY = articleWindowScrollY
loadWebView()
}
}
func focus() { func focus() {
webView?.becomeFirstResponder() webView?.becomeFirstResponder()
} }
@ -277,6 +299,9 @@ extension WebViewController: ArticleExtractorDelegate {
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
if articleExtractor?.state != .cancelled { if articleExtractor?.state != .cancelled {
self.extractedArticle = extractedArticle self.extractedArticle = extractedArticle
if let restoreWindowScrollY = restoreWindowScrollY {
windowScrollY = restoreWindowScrollY
}
isShowingExtractedArticle = true isShowingExtractedArticle = true
loadWebView() loadWebView()
articleExtractorButtonState = .on articleExtractorButtonState = .on
@ -591,6 +616,7 @@ private extension WebViewController {
} }
func startArticleExtractor() { func startArticleExtractor() {
guard articleExtractor == nil else { return }
if let link = article?.preferredLink, let extractor = ArticleExtractor(link) { if let link = article?.preferredLink, let extractor = ArticleExtractor(link) {
extractor.delegate = self extractor.delegate = self
extractor.process() extractor.process()

View File

@ -109,8 +109,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
var stateRestorationActivity: NSUserActivity { var stateRestorationActivity: NSUserActivity {
let activity = activityManager.stateRestorationActivity let activity = activityManager.stateRestorationActivity
var userInfo = activity.userInfo == nil ? [AnyHashable: Any]() : activity.userInfo var userInfo = activity.userInfo ?? [AnyHashable: Any]()
userInfo![UserInfoKey.windowState] = windowState()
userInfo[UserInfoKey.windowState] = windowState()
let articleState = articleViewController?.currentState
userInfo[UserInfoKey.isShowingExtractedArticle] = articleState?.isShowingExtractedArticle ?? false
userInfo[UserInfoKey.articleWindowScrollY] = articleState?.windowScrollY ?? 0
activity.userInfo = userInfo activity.userInfo = userInfo
return activity return activity
} }
@ -169,7 +175,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// * Once the article has loaded, navigate to the iPad home screen // * Once the article has loaded, navigate to the iPad home screen
// * While in landscape, select a feed and then select an article // * While in landscape, select a feed and then select an article
// * Install a fresh build of NNW to an iPad simulator (11 or 12.9' will do) running iPadOS 15 // * Install a fresh build of NNW to an iPad simulator (11 or 12.9' will do) running iPadOS 15
private var deferredFeedAndArticleSelect: (feedIndexPath: IndexPath, articleID: String)? private var deferredFeedAndArticleSelect: (feedIndexPath: IndexPath, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int)?
var timelineMiddleIndexPath: IndexPath? var timelineMiddleIndexPath: IndexPath?
@ -453,9 +459,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
rebuildBackingStores(initialLoad: true) rebuildBackingStores(initialLoad: true)
treeControllerDelegate.resetFilterExceptions() treeControllerDelegate.resetFilterExceptions()
if let (feedIndexPath, articleID) = deferredFeedAndArticleSelect { if let (feedIndexPath, articleID, isShowingExtractedArticle, articleWindowScrollY) = deferredFeedAndArticleSelect {
selectFeed(indexPath: feedIndexPath) { selectFeed(indexPath: feedIndexPath) {
self.selectArticleInCurrentFeed(articleID) self.selectArticleInCurrentFeed(articleID)
self.articleViewController?.setScrollPosition(isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
} }
} }
} }
@ -2254,14 +2261,14 @@ private extension SceneCoordinator {
guard let userInfo = userInfo else { return } guard let userInfo = userInfo else { return }
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let accountName = articlePathUserInfo[ArticlePathKey.accountName] as? String, let accountName = articlePathUserInfo[ArticlePathKey.accountName] as? String,
let webFeedID = articlePathUserInfo[ArticlePathKey.webFeedID] as? String, let webFeedID = articlePathUserInfo[ArticlePathKey.webFeedID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String, let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String,
let accountNode = findAccountNode(accountID: accountID, accountName: accountName), let accountNode = findAccountNode(accountID: accountID, accountName: accountName),
let account = accountNode.representedObject as? Account else { let account = accountNode.representedObject as? Account else {
return return
} }
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
@ -2280,20 +2287,21 @@ private extension SceneCoordinator {
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool { func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo),
return false let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool,
} let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else {
return false
}
switch feedIdentifier { switch feedIdentifier {
case .smartFeed: case .smartFeed:
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false } if let smartFeedNode = nodeFor(feedID: feedIdentifier) {
if let indexPath = indexPathFor(smartFeed) { let found = deferSelectFeedAndArticle(feedNode: smartFeedNode, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
selectFeed(indexPath: indexPath) { if found {
self.selectArticleInCurrentFeed(articleID) treeControllerDelegate.addFilterException(feedIdentifier)
} }
treeControllerDelegate.addFilterException(feedIdentifier) return found
return true
} }
case .script: case .script:
@ -2304,7 +2312,7 @@ private extension SceneCoordinator {
let folderNode = findFolderNode(folderName: folderName, beginningAt: accountNode) else { let folderNode = findFolderNode(folderName: folderName, beginningAt: accountNode) else {
return false return false
} }
let found = selectFeedAndArticle(feedNode: folderNode, articleID: articleID) let found = deferSelectFeedAndArticle(feedNode: folderNode, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
if found { if found {
treeControllerDelegate.addFilterException(feedIdentifier) treeControllerDelegate.addFilterException(feedIdentifier)
} }
@ -2314,7 +2322,7 @@ private extension SceneCoordinator {
guard let accountNode = findAccountNode(accountID: accountID), let webFeedNode = findWebFeedNode(webFeedID: webFeedID, beginningAt: accountNode) else { guard let accountNode = findAccountNode(accountID: accountID), let webFeedNode = findWebFeedNode(webFeedID: webFeedID, beginningAt: accountNode) else {
return false return false
} }
let found = selectFeedAndArticle(feedNode: webFeedNode, articleID: articleID) let found = deferSelectFeedAndArticle(feedNode: webFeedNode, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
if found { if found {
treeControllerDelegate.addFilterException(feedIdentifier) treeControllerDelegate.addFilterException(feedIdentifier)
if let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID { if let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID {
@ -2354,9 +2362,9 @@ private extension SceneCoordinator {
return nil return nil
} }
func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool { func deferSelectFeedAndArticle(feedNode: Node, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool {
if let feedIndexPath = indexPathFor(feedNode) { if let feedIndexPath = indexPathFor(feedNode) {
deferredFeedAndArticleSelect = (feedIndexPath, articleID) deferredFeedAndArticleSelect = (feedIndexPath, articleID, isShowingExtractedArticle, articleWindowScrollY)
return true return true
} }
return false return false