From 85f7adaa0a8e502a9d96528668c5864b73ad6b7e Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 2 May 2020 10:02:58 -0500 Subject: [PATCH] Use MainThreadOperation to prevent article status updates from interleaving --- .../Account/Account.xcodeproj/project.pbxproj | 14 +- .../CloudKit/CloudKitAccountDelegate.swift | 151 ++++-------------- .../CloudKitReceiveStatusOperation.swift | 50 ++++++ .../CloudKitRemoteNotificationOperation.swift | 52 ++++++ .../CloudKitSendStatusOperation.swift | 147 +++++++++++++++++ iOS/AppDelegate.swift | 40 +---- 6 files changed, 305 insertions(+), 149 deletions(-) create mode 100644 Frameworks/Account/CloudKit/CloudKitReceiveStatusOperation.swift create mode 100644 Frameworks/Account/CloudKit/CloudKitRemoteNotificationOperation.swift create mode 100644 Frameworks/Account/CloudKit/CloudKitSendStatusOperation.swift diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 0fdcc86f4..3c1de4b7c 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -89,6 +89,9 @@ 51E4DB302426353D0091EB5B /* CloudKitAccountZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */; }; 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; }; 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; }; + 51F6C58F245DB2E8001E41CA /* CloudKitSendStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6C58E245DB2E8001E41CA /* CloudKitSendStatusOperation.swift */; }; + 51F6C591245DB302001E41CA /* CloudKitReceiveStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6C590245DB302001E41CA /* CloudKitReceiveStatusOperation.swift */; }; + 51F6C593245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6C592245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift */; }; 552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; }; 552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; }; 552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; }; @@ -340,6 +343,9 @@ 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZone.swift; sourceTree = ""; }; 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; + 51F6C58E245DB2E8001E41CA /* CloudKitSendStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitSendStatusOperation.swift; sourceTree = ""; }; + 51F6C590245DB302001E41CA /* CloudKitReceiveStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitReceiveStatusOperation.swift; sourceTree = ""; }; + 51F6C592245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRemoteNotificationOperation.swift; sourceTree = ""; }; 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = ""; }; 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = ""; }; 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = ""; }; @@ -555,10 +561,13 @@ 5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */, 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */, 512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */, + 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */, 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */, 519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */, 5150FFFD243823B800C1A442 /* CloudKitError.swift */, - 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */, + 51F6C590245DB302001E41CA /* CloudKitReceiveStatusOperation.swift */, + 51F6C592245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift */, + 51F6C58E245DB2E8001E41CA /* CloudKitSendStatusOperation.swift */, 51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */, 51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */, ); @@ -1137,6 +1146,7 @@ 9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */, 9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */, 9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */, + 51F6C591245DB302001E41CA /* CloudKitReceiveStatusOperation.swift in Sources */, 9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */, 514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */, 9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */, @@ -1176,6 +1186,7 @@ 519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */, 9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */, 9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */, + 51F6C593245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift in Sources */, 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */, 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, 9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */, @@ -1280,6 +1291,7 @@ 179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */, 179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */, 179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */, + 51F6C58F245DB2E8001E41CA /* CloudKitSendStatusOperation.swift in Sources */, 179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift index 2ac45265a..685559120 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift @@ -18,10 +18,16 @@ import ArticlesDatabase import RSWeb import Secrets -public enum CloudKitAccountDelegateError: String, Error { - case invalidParameter = "An invalid parameter was used." +enum CloudKitAccountDelegateError: LocalizedError { + case invalidParameter + case unknown + + var errorDescription: String? { + return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.") + } } + final class CloudKitAccountDelegate: AccountDelegate { private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") @@ -33,10 +39,11 @@ final class CloudKitAccountDelegate: AccountDelegate { return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire") }() - private lazy var zones: [CloudKitZone] = [accountZone, articlesZone] private let accountZone: CloudKitAccountZone private let articlesZone: CloudKitArticlesZone + private let mainThreadOperationQueue = MainThreadOperationQueue() + private lazy var refresher: LocalAccountRefresher = { let refresher = LocalAccountRefresher() refresher.delegate = self @@ -63,21 +70,11 @@ final class CloudKitAccountDelegate: AccountDelegate { } func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { - os_log(.debug, log: log, "Processing remote notification...") - - let group = DispatchGroup() - - zones.forEach { zone in - group.enter() - zone.receiveRemoteNotification(userInfo: userInfo) { - group.leave() - } - } - - group.notify(queue: DispatchQueue.main) { - os_log(.debug, log: self.log, "Done processing remote notification...") + let op = CloudKitRemoteNotificationOperation(accountZone: accountZone, articlesZone: articlesZone, userInfo: userInfo) + op.completionBlock = { mainThreadOperaion in completion() } + mainThreadOperationQueue.add(op) } func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { @@ -101,17 +98,15 @@ final class CloudKitAccountDelegate: AccountDelegate { } func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - os_log(.debug, log: log, "Refreshing article statuses...") - - articlesZone.refreshArticles() { result in - os_log(.debug, log: self.log, "Done refreshing article statuses.") - switch result { - case .success: + let op = CloudKitReceiveStatusOperation(articlesZone: articlesZone) + op.completionBlock = { mainThreadOperaion in + if mainThreadOperaion.isCanceled { + completion(.failure(CloudKitAccountDelegateError.unknown)) + } else { completion(.success(())) - case .failure(let error): - completion(.failure(error)) } } + mainThreadOperationQueue.add(op) } func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { @@ -420,17 +415,15 @@ final class CloudKitAccountDelegate: AccountDelegate { os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription) } } - zones.forEach { zone in - zone.subscribeToZoneChanges() - } + accountZone.subscribeToZoneChanges() + articlesZone.subscribeToZoneChanges() } } func accountWillBeDeleted(_ account: Account) { - zones.forEach { zone in - zone.resetChangeToken() - } + accountZone.resetChangeToken() + articlesZone.resetChangeToken() } static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result) -> Void) { @@ -770,94 +763,19 @@ private extension CloudKitAccountDelegate { } func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result) -> Void)) { - os_log(.debug, log: log, "Sending article statuses...") - - database.selectForProcessing { result in - - func processStatuses(_ syncStatuses: [SyncStatus]) { - guard syncStatuses.count > 0 else { - os_log(.debug, log: self.log, "Done sending article statuses.") - completion(.success(())) - return - } - - let group = DispatchGroup() - let syncStatusChunks = syncStatuses.chunked(into: 300) - - if showProgress { - self.refreshProgress.addToNumberOfTasksAndRemaining(syncStatusChunks.count) - } - - for syncStatusChunk in syncStatusChunks { - group.enter() - self.sendArticleStatusChunk(for: account, syncStatuses: syncStatusChunk, showProgress: showProgress) { - group.leave() - } - } - - group.notify(queue: DispatchQueue.main) { - os_log(.debug, log: self.log, "Done sending article statuses.") - completion(.success(())) - } - - } - - switch result { - case .success(let syncStatuses): - processStatuses(syncStatuses) - case .failure(let databaseError): - completion(.failure(databaseError)) + let op = CloudKitSendStatusOperation(account: account, + articlesZone: articlesZone, + refreshProgress: refreshProgress, + showProgress: showProgress, + database: database) + op.completionBlock = { mainThreadOperaion in + if mainThreadOperaion.isCanceled { + completion(.failure(CloudKitAccountDelegateError.unknown)) + } else { + completion(.success(())) } } - } - - func sendArticleStatusChunk(for account: Account, syncStatuses: [SyncStatus], showProgress: Bool, completion: @escaping () -> Void) { - - let articleIDs = syncStatuses.map({ $0.articleID }) - account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in - - func processWithArticles(_ articles: Set
) { - - let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID }) - let articlesDict = articles.reduce(into: [String: Article]()) { result, article in - result[article.articleID] = article - } - let statusUpdates = syncStatusesDict.map { (key, value) in - return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key]) - } - - self.articlesZone.modifyArticles(statusUpdates) { result in - switch result { - case .success: - self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in - if showProgress { - self.refreshProgress.completeTask() - } - os_log(.debug, log: self.log, "Done sending article status block...") - completion() - } - case .failure(let error): - self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in - self.processAccountError(account, error) - os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) - completion() - } - } - } - - } - - switch result { - case .success(let articles): - processWithArticles(articles) - case .failure(let databaseError): - self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in - os_log(.error, log: self.log, "Send article status fetch articles error: %@.", databaseError.localizedDescription) - completion() - } - } - - } + mainThreadOperationQueue.add(op) } } @@ -875,3 +793,4 @@ extension CloudKitAccountDelegate: LocalAccountRefresherDelegate { } } + diff --git a/Frameworks/Account/CloudKit/CloudKitReceiveStatusOperation.swift b/Frameworks/Account/CloudKit/CloudKitReceiveStatusOperation.swift new file mode 100644 index 000000000..012942db5 --- /dev/null +++ b/Frameworks/Account/CloudKit/CloudKitReceiveStatusOperation.swift @@ -0,0 +1,50 @@ +// +// CloudKitReceiveStatusOperation.swift +// Account +// +// Created by Maurice Parker on 5/2/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import os.log +import RSCore + +class CloudKitReceiveStatusOperation: MainThreadOperation { + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") + + // MainThreadOperation + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? = "CloudKitReceiveStatusOperation" + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private weak var articlesZone: CloudKitArticlesZone? + + init(articlesZone: CloudKitArticlesZone) { + self.articlesZone = articlesZone + } + + func run() { + guard let articlesZone = articlesZone else { + self.operationDelegate?.operationDidComplete(self) + return + } + + os_log(.debug, log: log, "Refreshing article statuses...") + + articlesZone.refreshArticles() { result in + os_log(.debug, log: self.log, "Done refreshing article statuses.") + switch result { + case .success: + self.operationDelegate?.operationDidComplete(self) + case .failure(let error): + os_log(.error, log: self.log, "Receive status error: %@.", error.localizedDescription) + self.operationDelegate?.cancelOperation(self) + } + } + } + +} diff --git a/Frameworks/Account/CloudKit/CloudKitRemoteNotificationOperation.swift b/Frameworks/Account/CloudKit/CloudKitRemoteNotificationOperation.swift new file mode 100644 index 000000000..008b84846 --- /dev/null +++ b/Frameworks/Account/CloudKit/CloudKitRemoteNotificationOperation.swift @@ -0,0 +1,52 @@ +// +// CloudKitRemoteNotificationOperation.swift +// Account +// +// Created by Maurice Parker on 5/2/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +import os.log +import RSCore + +class CloudKitRemoteNotificationOperation: MainThreadOperation { + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") + + // MainThreadOperation + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? = "CloudKitRemoteNotificationOperation" + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private weak var accountZone: CloudKitAccountZone? + private weak var articlesZone: CloudKitArticlesZone? + private var userInfo: [AnyHashable : Any] + + init(accountZone: CloudKitAccountZone, articlesZone: CloudKitArticlesZone, userInfo: [AnyHashable : Any]) { + self.accountZone = accountZone + self.articlesZone = articlesZone + self.userInfo = userInfo + } + + func run() { + guard let accountZone = accountZone, let articlesZone = articlesZone else { + self.operationDelegate?.operationDidComplete(self) + return + } + + os_log(.debug, log: log, "Processing remote notification...") + + accountZone.receiveRemoteNotification(userInfo: userInfo) { + articlesZone.receiveRemoteNotification(userInfo: self.userInfo) { + os_log(.debug, log: self.log, "Done processing remote notification.") + self.operationDelegate?.operationDidComplete(self) + } + } + + } + +} diff --git a/Frameworks/Account/CloudKit/CloudKitSendStatusOperation.swift b/Frameworks/Account/CloudKit/CloudKitSendStatusOperation.swift new file mode 100644 index 000000000..a18585864 --- /dev/null +++ b/Frameworks/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -0,0 +1,147 @@ +// +// CloudKitSendStatusOperation.swift +// Account +// +// Created by Maurice Parker on 5/2/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import Articles +import os.log +import RSCore +import RSWeb +import SyncDatabase + +class CloudKitSendStatusOperation: MainThreadOperation { + + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") + + // MainThreadOperation + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? = "CloudKitSendStatusOperation" + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private weak var account: Account? + private weak var articlesZone: CloudKitArticlesZone? + private weak var refreshProgress: DownloadProgress? + private var showProgress: Bool + private var database: SyncDatabase + + init(account: Account, articlesZone: CloudKitArticlesZone, refreshProgress: DownloadProgress, showProgress: Bool, database: SyncDatabase) { + self.account = account + self.articlesZone = articlesZone + self.refreshProgress = refreshProgress + self.showProgress = showProgress + self.database = database + } + + func run() { + os_log(.debug, log: log, "Sending article statuses...") + + database.selectForProcessing { result in + + func processStatuses(_ syncStatuses: [SyncStatus]) { + guard syncStatuses.count > 0 else { + os_log(.debug, log: self.log, "Done sending article statuses.") + self.operationDelegate?.operationDidComplete(self) + return + } + + let group = DispatchGroup() + let syncStatusChunks = syncStatuses.chunked(into: 300) + + if self.showProgress { + self.refreshProgress?.addToNumberOfTasksAndRemaining(syncStatusChunks.count) + } + + for syncStatusChunk in syncStatusChunks { + group.enter() + self.sendArticleStatusChunk(syncStatuses: syncStatusChunk) { + group.leave() + } + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done sending article statuses.") + self.operationDelegate?.operationDidComplete(self) + } + + } + + switch result { + case .success(let syncStatuses): + processStatuses(syncStatuses) + case .failure(let databaseError): + os_log(.error, log: self.log, "Send status error: %@.", databaseError.localizedDescription) + self.operationDelegate?.cancelOperation(self) + } + } + + } + + func sendArticleStatusChunk(syncStatuses: [SyncStatus], completion: @escaping () -> Void) { + guard let account = account, let articlesZone = articlesZone else { + completion() + return + } + + let articleIDs = syncStatuses.map({ $0.articleID }) + account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in + + func processWithArticles(_ articles: Set
) { + + let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID }) + let articlesDict = articles.reduce(into: [String: Article]()) { result, article in + result[article.articleID] = article + } + let statusUpdates = syncStatusesDict.map { (key, value) in + return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key]) + } + + articlesZone.modifyArticles(statusUpdates) { result in + switch result { + case .success: + self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in + if self.showProgress { + self.refreshProgress?.completeTask() + } + os_log(.debug, log: self.log, "Done sending article status block...") + completion() + } + case .failure(let error): + self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in + self.processAccountError(account, error) + os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) + completion() + } + } + } + + } + + switch result { + case .success(let articles): + processWithArticles(articles) + case .failure(let databaseError): + self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in + os_log(.error, log: self.log, "Send article status fetch articles error: %@.", databaseError.localizedDescription) + completion() + } + } + + } + } + + func processAccountError(_ account: Account, _ error: Error) { + if case CloudKitZoneError.userDeletedZone = error { + account.removeFeeds(account.topLevelWebFeeds) + for folder in account.folders ?? Set() { + account.removeFolder(folder) + } + } + } + +} diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index ac41572b5..32205f2a0 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -24,7 +24,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid var syncTimer: ArticleStatusSyncTimer? - private let remoteNotificationOperationQueue = MainThreadOperationQueue() var shuttingDown = false { didSet { @@ -112,9 +111,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completion: @escaping (UIBackgroundFetchResult) -> Void) { - let op = RemoteNotificationOperation(userInfo: userInfo, completion: completion) - remoteNotificationOperationQueue.add(op) + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + DispatchQueue.main.async { + self.resumeDatabaseProcessingIfNecessary() + AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { + self.suspendApplication() + completionHandler(.newData) + } + } } func applicationWillTerminate(_ application: UIApplication) { @@ -391,31 +395,3 @@ private extension AppDelegate { } } - -class RemoteNotificationOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "RemoteNotificationOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private var userInfo: [AnyHashable : Any] - private var completion: (UIBackgroundFetchResult) -> Void - - init(userInfo: [AnyHashable : Any], completion: @escaping (UIBackgroundFetchResult) -> Void) { - self.userInfo = userInfo - self.completion = completion - } - - func run() { - appDelegate.resumeDatabaseProcessingIfNecessary() - AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { - appDelegate.suspendApplication() - self.completion(.newData) - self.operationDelegate?.operationDidComplete(self) - } - } - -}