From 2719772701bce9c1c48c40dd2d3559385b9ca85b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 21 Aug 2019 15:27:53 -0500 Subject: [PATCH] Port async timeline fetching from desktop NNW --- iOS/AppCoordinator.swift | 100 +++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/iOS/AppCoordinator.swift b/iOS/AppCoordinator.swift index f953dbc4e..56e978885 100644 --- a/iOS/AppCoordinator.swift +++ b/iOS/AppCoordinator.swift @@ -50,7 +50,8 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) - + private var fetchSerialNumber = 0 + private let fetchRequestQueue = FetchRequestQueue() private var articleRowMap = [String: Int]() // articleID: rowIndex private var animatingChanges = false @@ -107,14 +108,13 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { var timelineFetcher: ArticleFetcher? { didSet { - unreadCount = 0 currentArticleIndexPath = nil if timelineFetcher is Feed { showFeedNames = false } else { showFeedNames = true } - fetchArticles() + fetchAndReplaceArticlesSync() NotificationCenter.default.post(name: .ArticlesReinitialized, object: self, userInfo: nil) } } @@ -269,10 +269,16 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } @objc func accountStateDidChange(_ note: Notification) { + if timelineFetcherContainsAnyPseudoFeed() { + fetchAndReplaceArticlesSync() + } rebuildBackingStores() } @objc func accountsDidChange(_ note: Notification) { + if timelineFetcherContainsAnyPseudoFeed() { + fetchAndReplaceArticlesSync() + } rebuildBackingStores() } @@ -872,18 +878,6 @@ private extension AppCoordinator { // MARK: Fetching Articles - func fetchArticles() { - - guard let timelineFetcher = timelineFetcher else { - articles = ArticleArray() - return - } - - let fetchedArticles = timelineFetcher.fetchArticles() - updateArticles(with: fetchedArticles) - - } - func emptyTheTimeline() { if !articles.isEmpty { articles = [Article]() @@ -891,10 +885,10 @@ private extension AppCoordinator { } func sortDirectionDidChange() { - updateArticles(with: Set(articles)) + replaceArticles(with: Set(articles)) } - func updateArticles(with unsortedArticles: Set
) { + func replaceArticles(with unsortedArticles: Set
) { let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection) if articles != sortedArticles { articles = sortedArticles @@ -922,6 +916,8 @@ private extension AppCoordinator { } } + // MARK: Fetching Articles + func queueFetchAndMergeArticles() { fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) } @@ -932,20 +928,72 @@ private extension AppCoordinator { return } - var unsortedArticles = timelineFetcher.fetchArticles() - - // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. - let unsortedArticleIDs = unsortedArticles.articleIDs() - for article in articles { - if !unsortedArticleIDs.contains(article.articleID) { - unsortedArticles.insert(article) + fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (unsortedArticles) in + // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. + guard let strongSelf = self else { + return } + let unsortedArticleIDs = unsortedArticles.articleIDs() + var updatedArticles = unsortedArticles + for article in strongSelf.articles { + if !unsortedArticleIDs.contains(article.articleID) { + updatedArticles.insert(article) + } + } + + strongSelf.replaceArticles(with: updatedArticles) } - updateArticles(with: unsortedArticles) - } + func fetchAndReplaceArticlesSync() { + // To be called when the user has made a change of selection in the sidebar. + // It blocks the main thread, so that there’s no async delay, + // so that the entire display refreshes at once. + // It’s a better user experience this way. + cancelPendingAsyncFetches() + guard let timelineFetcher = timelineFetcher else { + emptyTheTimeline() + return + } + let fetchedArticles = fetchUnsortedArticlesSync(for: [timelineFetcher]) + replaceArticles(with: fetchedArticles) + } + + func cancelPendingAsyncFetches() { + fetchSerialNumber += 1 + fetchRequestQueue.cancelAllRequests() + } + + func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set
{ + cancelPendingAsyncFetches() + let articleFetchers = representedObjects.compactMap{ $0 as? ArticleFetcher } + if articleFetchers.isEmpty { + return Set
() + } + + var fetchedArticles = Set
() + for articleFetcher in articleFetchers { + fetchedArticles.formUnion(articleFetcher.fetchArticles()) + } + return fetchedArticles + } + + func fetchUnsortedArticlesAsync(for representedObjects: [Any], callback: @escaping ArticleSetBlock) { + // The callback will *not* be called if the fetch is no longer relevant — that is, + // if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called. + precondition(Thread.isMainThread) + cancelPendingAsyncFetches() + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in + precondition(Thread.isMainThread) + guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { + return + } + callback(articles) + } + fetchRequestQueue.add(fetchOperation) + } + func timelineFetcherContainsAnyPseudoFeed() -> Bool { if timelineFetcher is PseudoFeed { return true