From 27d27cbf1ae8552015a037a88682a1e4ea64d56c Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 25 Mar 2024 21:10:37 -0700 Subject: [PATCH] Mark many things as MainActor and deal with the fallout. --- Account/Sources/Account/Account.swift | 27 +- Account/Sources/Account/AccountDelegate.swift | 2 +- Account/Sources/Account/AccountError.swift | 32 +- Account/Sources/Account/AccountManager.swift | 2 +- .../CloudKit/CloudKitAccountDelegate.swift | 67 ++-- .../CloudKit/CloudKitAccountZone.swift | 3 +- .../CloudKitAccountZoneDelegate.swift | 5 +- .../CloudKitSendStatusOperation.swift | 12 +- Account/Sources/Account/Container.swift | 6 +- Account/Sources/Account/ContainerPath.swift | 2 +- Account/Sources/Account/Feed.swift | 6 +- .../Feedbin/FeedbinAccountDelegate.swift | 359 +++++++++--------- .../Feedly/FeedlyAccountDelegate.swift | 4 +- .../Feedly/FeedlyAccountDelegateError.swift | 30 +- .../Feedly/FeedlyFeedContainerValidator.swift | 4 +- .../OAuthAccountAuthorizationOperation.swift | 2 +- Account/Sources/Account/Folder.swift | 4 +- .../LocalAccount/LocalAccountRefresher.swift | 26 +- .../NewsBlurAccountDelegate+Internal.swift | 140 +++---- .../NewsBlur/NewsBlurAccountDelegate.swift | 207 +++++----- .../ReaderAPI/ReaderAPIAccountDelegate.swift | 314 +++++++-------- .../Account/ReaderAPI/ReaderAPICaller.swift | 2 +- Core/Sources/Core/DisplayNameProvider.swift | 4 +- Core/Sources/Core/MainThreadOperation.swift | 2 +- Mac/MainWindow/Sidebar/PasteboardFeed.swift | 2 +- Mac/MainWindow/Sidebar/PasteboardFolder.swift | 6 +- Mac/Scriptability/Account+Scriptability.swift | 2 +- Mac/Scriptability/Feed+Scriptability.swift | 18 +- Mac/Scriptability/Folder+Scriptability.swift | 2 +- .../Extensions/AddFeedDefaultContainer.swift | 2 +- Shared/Favicons/FaviconDownloader.swift | 4 +- .../ShareExtension/ExtensionContainers.swift | 4 +- .../SmartFeedPasteboardWriter.swift | 2 +- .../Sources/SyncDatabase/SyncDatabase.swift | 14 +- 34 files changed, 693 insertions(+), 625 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 9ff2cb19e..5cd088bf3 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -62,7 +62,7 @@ public enum FetchType { case searchWithArticleIDs(String, Set) } -public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable { +@MainActor public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable { public struct UserInfoKey { public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount @@ -894,12 +894,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return } database.createStatusesIfNeeded(articleIDs: articleIDs) { error in - if let error = error { - completion?(error) - return + + MainActor.assumeIsolated { + if let error = error { + completion?(error) + return + } + self.noteStatusesForArticleIDsDidChange(articleIDs) + completion?(nil) } - self.noteStatusesForArticleIDsDidChange(articleIDs) - completion?(nil) } } @@ -912,11 +915,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return } database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in - if let error { - completion?(error) - } else { - self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag) - completion?(nil) + MainActor.assumeIsolated { + if let error { + completion?(error) + } else { + self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag) + completion?(nil) + } } } } diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index 66e52210e..c1cef58d7 100644 --- a/Account/Sources/Account/AccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegate.swift @@ -11,7 +11,7 @@ import Articles import RSWeb import Secrets -protocol AccountDelegate { +@MainActor protocol AccountDelegate { var behaviors: AccountBehaviors { get } diff --git a/Account/Sources/Account/AccountError.swift b/Account/Sources/Account/AccountError.swift index 75852b178..ddd64c2b2 100644 --- a/Account/Sources/Account/AccountError.swift +++ b/Account/Sources/Account/AccountError.swift @@ -14,18 +14,22 @@ public enum AccountError: LocalizedError { case createErrorNotFound case createErrorAlreadySubscribed case opmlImportInProgress - case wrappedError(error: Error, account: Account) - - public var account: Account? { - if case .wrappedError(_, let account) = self { - return account + case wrappedError(error: Error, accountID: String, accountName: String) + + @MainActor public var account: Account? { + if case .wrappedError(_, let accountID, _) = self { + return AccountManager.shared.existingAccount(with: accountID) } else { return nil } } - public var isCredentialsError: Bool { - if case .wrappedError(let error, _) = self { + @MainActor public static func wrappedError(error: Error, account: Account) -> AccountError { + wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay) + } + + @MainActor public var isCredentialsError: Bool { + if case .wrappedError(let error, _, _) = self { if case TransportError.httpError(let status) = error { return isCredentialsError(status: status) } @@ -41,17 +45,17 @@ public enum AccountError: LocalizedError { return NSLocalizedString("You are already subscribed to this feed and can’t add it again.", comment: "Already subscribed") case .opmlImportInProgress: return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running") - case .wrappedError(let error, let account): + case .wrappedError(let error, _, let accountName): switch error { case TransportError.httpError(let status): if isCredentialsError(status: status) { let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired") - return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay) as String + return NSString.localizedStringWithFormat(localizedText as NSString, accountName) as String } else { - return unknownError(error, account) + return unknownError(error, accountName) } default: - return unknownError(error, account) + return unknownError(error, accountName) } } } @@ -62,7 +66,7 @@ public enum AccountError: LocalizedError { return nil case .createErrorAlreadySubscribed: return nil - case .wrappedError(let error, _): + case .wrappedError(let error, _, _): switch error { case TransportError.httpError(let status): if isCredentialsError(status: status) { @@ -84,9 +88,9 @@ public enum AccountError: LocalizedError { private extension AccountError { - func unknownError(_ error: Error, _ account: Account) -> String { + func unknownError(_ error: Error, _ accountName: String) -> String { let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error") - return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay, error.localizedDescription) as String + return NSString.localizedStringWithFormat(localizedText as NSString, accountName, error.localizedDescription) as String } func isCredentialsError(status: Int) -> Bool { diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index abde11ca0..de7a1ae2f 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -15,7 +15,7 @@ import Secrets // Main thread only. -public final class AccountManager: UnreadCountProvider { +@MainActor public final class AccountManager: UnreadCountProvider { @MainActor public static var shared: AccountManager! public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml" diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index b92f08324..0208c8776 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -28,7 +28,7 @@ enum CloudKitAccountDelegateError: LocalizedError { } } -final class CloudKitAccountDelegate: AccountDelegate { +@MainActor final class CloudKitAccountDelegate: AccountDelegate { private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") @@ -416,10 +416,12 @@ final class CloudKitAccountDelegate: AccountDelegate { self.database.insertStatuses(syncStatuses) { _ in self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { - self.sendArticleStatus(for: account, showProgress: false) { _ in } + MainActor.assumeIsolated { + if let count = try? result.get(), count > 100 { + self.sendArticleStatus(for: account, showProgress: false) { _ in } + } + completion(.success(())) } - completion(.success(())) } } case .failure(let error): @@ -648,35 +650,36 @@ private extension CloudKitAccountDelegate { if let parsedFeed = parsedFeed { account.update(feed, with: parsedFeed) { result in - switch result { - case .success: - - self.accountZone.createFeed(url: bestFeedSpecifier.urlString, - name: parsedFeed.title, - editedName: editedName, - homePageURL: parsedFeed.homePageURL, - container: container) { result in - - self.refreshProgress.completeTask() - switch result { - case .success(let externalID): - feed.externalID = externalID - self.sendNewArticlesToTheCloud(account, feed) - completion(.success(feed)) - case .failure(let error): - container.removeFeed(feed) - self.refreshProgress.completeTasks(2) - completion(.failure(error)) + MainActor.assumeIsolated { + switch result { + case .success: + + self.accountZone.createFeed(url: bestFeedSpecifier.urlString, + name: parsedFeed.title, + editedName: editedName, + homePageURL: parsedFeed.homePageURL, + container: container) { result in + + self.refreshProgress.completeTask() + switch result { + case .success(let externalID): + feed.externalID = externalID + self.sendNewArticlesToTheCloud(account, feed) + completion(.success(feed)) + case .failure(let error): + container.removeFeed(feed) + self.refreshProgress.completeTasks(2) + completion(.failure(error)) + } + } + case .failure(let error): + container.removeFeed(feed) + self.refreshProgress.completeTasks(3) + completion(.failure(error)) } - - case .failure(let error): - container.removeFeed(feed) - self.refreshProgress.completeTasks(3) - completion(.failure(error)) } - } } else { self.refreshProgress.completeTasks(3) @@ -700,9 +703,9 @@ private extension CloudKitAccountDelegate { } func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) { - + Task { @MainActor in - + do { let articles = try await account.articles(for: .feed(feed)) self.storeArticleChanges(new: articles, updated: Set
(), deleted: Set
()) { @@ -716,7 +719,7 @@ private extension CloudKitAccountDelegate { } } } - + } catch { os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription) } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index 118f177e6..4b5cc8400 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -20,7 +20,8 @@ enum CloudKitAccountZoneError: LocalizedError { return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.") } } -final class CloudKitAccountZone: CloudKitZone { + +@MainActor final class CloudKitAccountZone: CloudKitZone { var zoneID: CKRecordZone.ID diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift index 1ddffaa5a..5eb2857be 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -13,8 +13,8 @@ import CloudKit import Articles import CloudKitExtras -class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { - +@MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { + private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String) private var newUnclaimedFeeds = [String: [UnclaimedFeed]]() private var existingUnclaimedFeeds = [String: [Feed]]() @@ -135,7 +135,6 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { account?.removeFolder(folder) } } - } private extension CloudKitAcountZoneDelegate { diff --git a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift index a96a86738..38e531885 100644 --- a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation { } } - func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) { + @MainActor func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) { guard let account = account, let articlesZone = articlesZone else { completion(true) return @@ -156,9 +156,11 @@ private extension CloudKitSendStatusOperation { } 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(true) + MainActor.assumeIsolated { + self.processAccountError(account, error) + os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) + completion(true) + } } } } @@ -178,7 +180,7 @@ private extension CloudKitSendStatusOperation { } } - func processAccountError(_ account: Account, _ error: Error) { + @MainActor func processAccountError(_ account: Account, _ error: Error) { if case CloudKitZoneError.userDeletedZone = error { account.removeFeeds(account.topLevelFeeds) for folder in account.folders ?? Set() { diff --git a/Account/Sources/Account/Container.swift b/Account/Sources/Account/Container.swift index adb37f35a..f4cce4384 100644 --- a/Account/Sources/Account/Container.swift +++ b/Account/Sources/Account/Container.swift @@ -15,20 +15,20 @@ extension Notification.Name { public static let ChildrenDidChange = Notification.Name("ChildrenDidChange") } -public protocol Container: AnyObject, ContainerIdentifiable { +@MainActor public protocol Container: AnyObject, ContainerIdentifiable { var account: Account? { get } var topLevelFeeds: Set { get set } var folders: Set? { get set } var externalID: String? { get set } - + func hasAtLeastOneFeed() -> Bool func objectIsChild(_ object: AnyObject) -> Bool func hasChildFolder(with: String) -> Bool func childFolder(with: String) -> Folder? - func removeFeed(_ feed: Feed) + func removeFeed(_ feed: Feed) func addFeed(_ feed: Feed) //Recursive — checks subfolders diff --git a/Account/Sources/Account/ContainerPath.swift b/Account/Sources/Account/ContainerPath.swift index ea48838f2..c9f7fe4db 100644 --- a/Account/Sources/Account/ContainerPath.swift +++ b/Account/Sources/Account/ContainerPath.swift @@ -12,7 +12,7 @@ import Foundation // Mainly used with deleting objects and undo/redo. // Especially redo. The idea is to put something back in the right place. -public struct ContainerPath { +@MainActor public struct ContainerPath { private weak var account: Account? private let names: [String] // empty if top-level of account diff --git a/Account/Sources/Account/Feed.swift b/Account/Sources/Account/Feed.swift index 14c5db871..a2ea34164 100644 --- a/Account/Sources/Account/Feed.swift +++ b/Account/Sources/Account/Feed.swift @@ -11,7 +11,7 @@ import RSWeb import Articles import Core -public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable { +@MainActor public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable { public weak var account: Account? public let url: String @@ -294,11 +294,11 @@ extension Feed: OPMLRepresentable { extension Set where Element == Feed { - func feedIDs() -> Set { + @MainActor func feedIDs() -> Set { return Set(map { $0.feedID }) } - func sorted() -> Array { + @MainActor func sorted() -> Array { return sorted(by: { (feed1, feed2) -> Bool in if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame { return feed1.url < feed2.url diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift index f30e1971b..1c2840704 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift @@ -136,62 +136,64 @@ final class FeedbinAccountDelegate: AccountDelegate { database.selectForProcessing { result in - func processStatuses(_ syncStatuses: [SyncStatus]) { - let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } - let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } - let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } - let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } - - let group = DispatchGroup() - var errorOccurred = false - - group.enter() - self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in - group.leave() - if case .failure = result { - errorOccurred = true + MainActor.assumeIsolated { + @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { + let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } + let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } + let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } + let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } + + let group = DispatchGroup() + var errorOccurred = false + + group.enter() + self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done sending article statuses.") + if errorOccurred { + completion(.failure(FeedbinAccountDelegateError.unknown)) + } else { + completion(.success(())) + } } } - - group.enter() - self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } + + switch result { + case .success(let syncStatuses): + processStatuses(syncStatuses) + case .failure(let databaseError): + completion(.failure(databaseError)) } - - group.enter() - self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } - } - - group.enter() - self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } - } - - group.notify(queue: DispatchQueue.main) { - os_log(.debug, log: self.log, "Done sending article statuses.") - if errorOccurred { - completion(.failure(FeedbinAccountDelegateError.unknown)) - } else { - completion(.success(())) - } - } - } - - switch result { - case .success(let syncStatuses): - processStatuses(syncStatuses) - case .failure(let databaseError): - completion(.failure(databaseError)) } } } @@ -564,10 +566,12 @@ final class FeedbinAccountDelegate: AccountDelegate { self.database.insertStatuses(syncStatuses) { _ in self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { - self.sendArticleStatus(for: account) { _ in } + MainActor.assumeIsolated { + if let count = try? result.get(), count > 100 { + self.sendArticleStatus(for: account) { _ in } + } + completion(.success(())) } - completion(.success(())) } } case .failure(let error): @@ -1082,43 +1086,46 @@ private extension FeedbinAccountDelegate { case .success(let (entries, page)): self.processEntries(account: account, entries: entries) { error in + + MainActor.assumeIsolated { if let error = error { completion(.failure(error)) return } - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - - self.refreshArticles(account, page: page, updateFetchDate: nil) { result in - switch result { - case .success: - - self.refreshProgress.completeTask() - self.refreshMissingArticles(account) { result in - switch result { - case .success: - - self.refreshProgress.completeTask() - DispatchQueue.main.async { - completion(.success(feed)) + self.refreshArticleStatus(for: account) { result in + switch result { + case .success: + + self.refreshArticles(account, page: page, updateFetchDate: nil) { result in + switch result { + case .success: + + self.refreshProgress.completeTask() + self.refreshMissingArticles(account) { result in + switch result { + case .success: + + self.refreshProgress.completeTask() + DispatchQueue.main.async { + completion(.success(feed)) + } + + case .failure(let error): + completion(.failure(error)) } - case .failure(let error): - completion(.failure(error)) } + case .failure(let error): + completion(.failure(error)) } - case .failure(let error): - completion(.failure(error)) } + case .failure(let error): + completion(.failure(error)) } - - case .failure(let error): - completion(.failure(error)) } } } @@ -1145,21 +1152,24 @@ private extension FeedbinAccountDelegate { } self.processEntries(account: account, entries: entries) { error in - - self.refreshProgress.completeTask() - if let error = error { - completion(.failure(error)) - return - } + MainActor.assumeIsolated { - self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in - os_log(.debug, log: self.log, "Done refreshing articles.") - switch result { - case .success: - completion(.success(())) - case .failure(let error): + self.refreshProgress.completeTask() + + if let error = error { completion(.failure(error)) + return + } + + self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in + os_log(.debug, log: self.log, "Done refreshing articles.") + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } } } } @@ -1250,16 +1260,18 @@ private extension FeedbinAccountDelegate { case .success(let (entries, nextPage)): self.processEntries(account: account, entries: entries) { error in - self.refreshProgress.completeTask() + MainActor.assumeIsolated { + self.refreshProgress.completeTask() - if let error = error { - completion(.failure(error)) - return + if let error = error { + completion(.failure(error)) + return + } + + self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion) } - - self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion) } - + case .failure(let error): completion(.failure(error)) } @@ -1294,47 +1306,50 @@ private extension FeedbinAccountDelegate { database.selectPendingReadStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { - - let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } ) - let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs) - - account.fetchUnreadArticleIDs { articleIDsResult in - guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { - return + MainActor.assumeIsolated { + + @MainActor func process(_ pendingArticleIDs: Set) { + + let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } ) + let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs) + + account.fetchUnreadArticleIDs { articleIDsResult in + MainActor.assumeIsolated { + guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { + return + } + + let group = DispatchGroup() + + // Mark articles as unread + let deltaUnreadArticleIDs = updatableFeedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs) + group.enter() + account.markAsUnread(deltaUnreadArticleIDs) { _ in + group.leave() + } + + // Mark articles as read + let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableFeedbinUnreadArticleIDs) + group.enter() + account.markAsRead(deltaReadArticleIDs) { _ in + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } } - let group = DispatchGroup() - - // Mark articles as unread - let deltaUnreadArticleIDs = updatableFeedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs) - group.enter() - account.markAsUnread(deltaUnreadArticleIDs) { _ in - group.leave() - } - - // Mark articles as read - let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableFeedbinUnreadArticleIDs) - group.enter() - account.markAsRead(deltaReadArticleIDs) { _ in - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - completion() - } - } + switch result { + case .success(let pendingArticleIDs): + process(pendingArticleIDs) + case .failure(let error): + os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription) + } } - - switch result { - case .success(let pendingArticleIDs): - process(pendingArticleIDs) - case .failure(let error): - os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription) - } - } } @@ -1347,46 +1362,50 @@ private extension FeedbinAccountDelegate { database.selectPendingStarredStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { - - let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } ) - let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs) - - account.fetchStarredArticleIDs { articleIDsResult in - guard let currentStarredArticleIDs = try? articleIDsResult.get() else { - return - } - - let group = DispatchGroup() + MainActor.assumeIsolated { + @MainActor func process(_ pendingArticleIDs: Set) { - // Mark articles as starred - let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs) - group.enter() - account.markAsStarred(deltaStarredArticleIDs) { _ in - group.leave() - } - - // Mark articles as unstarred - let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs) - group.enter() - account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - completion() + let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } ) + let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs) + + account.fetchStarredArticleIDs { articleIDsResult in + + MainActor.assumeIsolated { + guard let currentStarredArticleIDs = try? articleIDsResult.get() else { + return + } + + let group = DispatchGroup() + + // Mark articles as starred + let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs) + group.enter() + account.markAsStarred(deltaStarredArticleIDs) { _ in + group.leave() + } + + // Mark articles as unstarred + let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs) + group.enter() + account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } } + + } + + switch result { + case .success(let pendingArticleIDs): + process(pendingArticleIDs) + case .failure(let error): + os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription) } - } - - switch result { - case .success(let pendingArticleIDs): - process(pendingArticleIDs) - case .failure(let error): - os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription) - } - } } diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 7e18c13a3..8e6edee41 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -408,7 +408,7 @@ final class FeedlyAccountDelegate: AccountDelegate { func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { guard let folder = container as? Folder, let collectionId = folder.externalID else { return DispatchQueue.main.async { - completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed))) + completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed.nameForDisplay))) } } @@ -442,7 +442,7 @@ final class FeedlyAccountDelegate: AccountDelegate { completion(.success(())) case .failure: from.addFeed(feed) - completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to))) + completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay))) } } case .failure(let error): diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift index 0ed5cac99..535112b52 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift @@ -14,12 +14,12 @@ enum FeedlyAccountDelegateError: LocalizedError { case unableToAddFolder(String) case unableToRenameFolder(String, String) case unableToRemoveFolder(String) - case unableToMoveFeedBetweenFolders(Feed, Folder, Folder) + case unableToMoveFeedBetweenFolders(String, String, String) case addFeedChooseFolder - case addFeedInvalidFolder(Folder) + case addFeedInvalidFolder(String) case unableToRenameFeed(String, String) - case unableToRemoveFeed(Feed) - + case unableToRemoveFeed(String) + var errorDescription: String? { switch self { case .notLoggedIn: @@ -41,24 +41,24 @@ enum FeedlyAccountDelegateError: LocalizedError { let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly – Could not remove a folder/collection.") return String(format: template, name) - case .unableToMoveFeedBetweenFolders(let feed, _, let to): + case .unableToMoveFeedBetweenFolders(let feedName, _, let destinationFolderName): let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly – Could not move a feed between folders/collections.") - return String(format: template, feed.nameForDisplay, to.nameForDisplay) - + return String(format: template, feedName, destinationFolderName) + case .addFeedChooseFolder: return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly – Feed can only be added to folders.") - case .addFeedInvalidFolder(let invalidFolder): + case .addFeedInvalidFolder(let folderName): let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly – Feed can only be added to folders.") - return String(format: template, invalidFolder.nameForDisplay) - + return String(format: template, folderName) + case .unableToRenameFeed(let from, let to): let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a feed.") return String(format: template, from, to) - case .unableToRemoveFeed(let feed): + case .unableToRemoveFeed(let feedName): let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly – Could not remove a feed.") - return String(format: template, feed.nameForDisplay) + return String(format: template, feedName) } } @@ -80,10 +80,10 @@ enum FeedlyAccountDelegateError: LocalizedError { case .unableToRemoveFolder: return nil - case .unableToMoveFeedBetweenFolders(let feed, let from, let to): + case .unableToMoveFeedBetweenFolders(let feedName, let sourceFolderName, let destinationFolderName): let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly – Could not move a feed between folders/collections.") - return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay) - + return String(format: template, feedName, sourceFolderName, destinationFolderName) + case .addFeedChooseFolder: return nil diff --git a/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift b/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift index da4e6fb81..f26f39aac 100644 --- a/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift +++ b/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift @@ -8,7 +8,7 @@ import Foundation -struct FeedlyFeedContainerValidator { +@MainActor struct FeedlyFeedContainerValidator { var container: Container func getValidContainer() throws -> (Folder, String) { @@ -17,7 +17,7 @@ struct FeedlyFeedContainerValidator { } guard let collectionId = folder.externalID else { - throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder) + throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder.nameForDisplay) } return (folder, collectionId) diff --git a/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift b/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift index 21ada7d3c..b5dcd9831 100644 --- a/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift +++ b/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -23,7 +23,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { return NSLocalizedString("There is already a Feedly account with that username created.", comment: "Duplicate Error") } } -@objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding { +@MainActor @objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding { public var isCanceled: Bool = false { didSet { diff --git a/Account/Sources/Account/Folder.swift b/Account/Sources/Account/Folder.swift index 4c27f1248..713496ede 100644 --- a/Account/Sources/Account/Folder.swift +++ b/Account/Sources/Account/Folder.swift @@ -10,7 +10,7 @@ import Foundation import Articles import Core -public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable { +@MainActor public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable { public var containerID: ContainerIdentifier? { guard let accountID = account?.accountID else { @@ -198,7 +198,7 @@ extension Folder: OPMLRepresentable { extension Set where Element == Folder { - func sorted() -> Array { + @MainActor func sorted() -> Array { return sorted(by: { (folder1, folder2) -> Bool in return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending }) diff --git a/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift b/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift index 799051495..e685266d7 100644 --- a/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift +++ b/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift @@ -52,7 +52,7 @@ final class LocalAccountRefresher { extension LocalAccountRefresher: DownloadSessionDelegate { - func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? { + @MainActor func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? { guard let feed = representedObject as? Feed else { return nil } @@ -68,7 +68,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate { return request } - func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) { + @MainActor func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) { let feed = representedObject as! Feed guard !data.isEmpty, !isSuspended else { @@ -101,18 +101,20 @@ extension LocalAccountRefresher: DownloadSessionDelegate { } account.update(feed, with: parsedFeed) { result in - if case .success(let articleChanges) = result { - if let httpResponse = response as? HTTPURLResponse { - feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse) - } - feed.contentHash = dataHash - self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) - self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) { + MainActor.assumeIsolated { + if case .success(let articleChanges) = result { + if let httpResponse = response as? HTTPURLResponse { + feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse) + } + feed.contentHash = dataHash + self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) + self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) { + completion() + } + } else { completion() + self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) } - } else { - completion() - self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) } } diff --git a/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift b/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift index 17ff0ca70..14c8034c2 100644 --- a/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift +++ b/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift @@ -328,45 +328,49 @@ extension NewsBlurAccountDelegate { completion() return } - + database.selectPendingReadStatusArticleIDs() { result in - func process(_ pendingStoryHashes: Set) { - - let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } ) - let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes) - - account.fetchUnreadArticleIDs { articleIDsResult in - guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { - return - } - - let group = DispatchGroup() + MainActor.assumeIsolated { + @MainActor func process(_ pendingStoryHashes: Set) { - // Mark articles as unread - let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs) - group.enter() - account.markAsUnread(deltaUnreadArticleIDs) { _ in - group.leave() - } - - // Mark articles as read - let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes) - group.enter() - account.markAsRead(deltaReadArticleIDs) { _ in - group.leave() - } + let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } ) + let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes) - group.notify(queue: DispatchQueue.main) { - completion() + account.fetchUnreadArticleIDs { articleIDsResult in + MainActor.assumeIsolated { + guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { + return + } + + let group = DispatchGroup() + + // Mark articles as unread + let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs) + group.enter() + account.markAsUnread(deltaUnreadArticleIDs) { _ in + group.leave() + } + + // Mark articles as read + let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes) + group.enter() + account.markAsRead(deltaReadArticleIDs) { _ in + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } } } - } - - switch result { - case .success(let pendingArticleIDs): - process(pendingArticleIDs) - case .failure(let error): - os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription) + + switch result { + case .success(let pendingArticleIDs): + process(pendingArticleIDs) + case .failure(let error): + os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription) + } } } } @@ -378,43 +382,47 @@ extension NewsBlurAccountDelegate { } database.selectPendingStarredStatusArticleIDs() { result in - func process(_ pendingStoryHashes: Set) { + MainActor.assumeIsolated { + @MainActor func process(_ pendingStoryHashes: Set) { - let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } ) - let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes) + let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } ) + let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes) - account.fetchStarredArticleIDs { articleIDsResult in - guard let currentStarredArticleIDs = try? articleIDsResult.get() else { - return - } - - let group = DispatchGroup() - - // Mark articles as starred - let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs) - group.enter() - account.markAsStarred(deltaStarredArticleIDs) { _ in - group.leave() - } - - // Mark articles as unstarred - let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes) - group.enter() - account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - completion() + account.fetchStarredArticleIDs { articleIDsResult in + MainActor.assumeIsolated { + guard let currentStarredArticleIDs = try? articleIDsResult.get() else { + return + } + + let group = DispatchGroup() + + // Mark articles as starred + let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs) + group.enter() + account.markAsStarred(deltaStarredArticleIDs) { _ in + group.leave() + } + + // Mark articles as unstarred + let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes) + group.enter() + account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } } } - } - switch result { - case .success(let pendingArticleIDs): - process(pendingArticleIDs) - case .failure(let error): - os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription) + switch result { + case .success(let pendingArticleIDs): + process(pendingArticleIDs) + case .failure(let error): + os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription) + } } } } diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift index 86f9d8f1d..de8ee5221 100644 --- a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift @@ -8,7 +8,7 @@ import Articles import Database -import RSParser +@preconcurrency import RSParser import RSWeb import SyncDatabase import os.log @@ -136,70 +136,73 @@ final class NewsBlurAccountDelegate: AccountDelegate { database.selectForProcessing { result in - func processStatuses(_ syncStatuses: [SyncStatus]) { - let createUnreadStatuses = syncStatuses.filter { - $0.key == SyncStatus.Key.read && $0.flag == false - } - let deleteUnreadStatuses = syncStatuses.filter { - $0.key == SyncStatus.Key.read && $0.flag == true - } - let createStarredStatuses = syncStatuses.filter { - $0.key == SyncStatus.Key.starred && $0.flag == true - } - let deleteStarredStatuses = syncStatuses.filter { - $0.key == SyncStatus.Key.starred && $0.flag == false - } - - let group = DispatchGroup() - var errorOccurred = false - - group.enter() - self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in - group.leave() - if case .failure = result { - errorOccurred = true + MainActor.assumeIsolated { + + @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { + let createUnreadStatuses = syncStatuses.filter { + $0.key == SyncStatus.Key.read && $0.flag == false + } + let deleteUnreadStatuses = syncStatuses.filter { + $0.key == SyncStatus.Key.read && $0.flag == true + } + let createStarredStatuses = syncStatuses.filter { + $0.key == SyncStatus.Key.starred && $0.flag == true + } + let deleteStarredStatuses = syncStatuses.filter { + $0.key == SyncStatus.Key.starred && $0.flag == false + } + + let group = DispatchGroup() + var errorOccurred = false + + group.enter() + self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done sending article statuses.") + if errorOccurred { + completion(.failure(NewsBlurError.unknown)) + } else { + completion(.success(())) + } } } - - group.enter() - self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } + + switch result { + case .success(let syncStatuses): + processStatuses(syncStatuses) + case .failure(let databaseError): + completion(.failure(databaseError)) } - - group.enter() - self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } - } - - group.enter() - self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } - } - - group.notify(queue: DispatchQueue.main) { - os_log(.debug, log: self.log, "Done sending article statuses.") - if errorOccurred { - completion(.failure(NewsBlurError.unknown)) - } else { - completion(.success(())) - } - } - } - - switch result { - case .success(let syncStatuses): - processStatuses(syncStatuses) - case .failure(let databaseError): - completion(.failure(databaseError)) } } } @@ -272,53 +275,55 @@ final class NewsBlurAccountDelegate: AccountDelegate { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in - func process(_ fetchedHashes: Set) { - let group = DispatchGroup() - var errorOccurred = false + MainActor.assumeIsolated { + @MainActor func process(_ fetchedHashes: Set) { + let group = DispatchGroup() + var errorOccurred = false - let storyHashes = Array(fetchedHashes).map { - NewsBlurStoryHash(hash: $0, timestamp: Date()) - } - let chunkedStoryHashes = storyHashes.chunked(into: 100) + let storyHashes = Array(fetchedHashes).map { + NewsBlurStoryHash(hash: $0, timestamp: Date()) + } + let chunkedStoryHashes = storyHashes.chunked(into: 100) - for chunk in chunkedStoryHashes { - group.enter() - self.caller.retrieveStories(hashes: chunk) { result in + for chunk in chunkedStoryHashes { + group.enter() + self.caller.retrieveStories(hashes: chunk) { result in - switch result { - case .success((let stories, _)): - self.processStories(account: account, stories: stories) { result in - group.leave() - if case .failure = result { - errorOccurred = true + switch result { + case .success((let stories, _)): + self.processStories(account: account, stories: stories) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } } + case .failure(let error): + errorOccurred = true + os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription) + group.leave() } - case .failure(let error): - errorOccurred = true - os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription) - group.leave() + } + } + + group.notify(queue: DispatchQueue.main) { + self.refreshProgress.completeTask() + os_log(.debug, log: self.log, "Done refreshing missing stories.") + if errorOccurred { + completion(.failure(NewsBlurError.unknown)) + } else { + completion(.success(())) } } } - group.notify(queue: DispatchQueue.main) { + switch result { + case .success(let fetchedArticleIDs): + process(fetchedArticleIDs) + case .failure(let error): self.refreshProgress.completeTask() - os_log(.debug, log: self.log, "Done refreshing missing stories.") - if errorOccurred { - completion(.failure(NewsBlurError.unknown)) - } else { - completion(.success(())) - } + completion(.failure(error)) } } - - switch result { - case .success(let fetchedArticleIDs): - process(fetchedArticleIDs) - case .failure(let error): - self.refreshProgress.completeTask() - completion(.failure(error)) - } } } @@ -591,10 +596,12 @@ final class NewsBlurAccountDelegate: AccountDelegate { self.database.insertStatuses(syncStatuses) { _ in self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { - self.sendArticleStatus(for: account) { _ in } + MainActor.assumeIsolated { + if let count = try? result.get(), count > 100 { + self.sendArticleStatus(for: account) { _ in } + } + completion(.success(())) } - completion(.success(())) } } case .failure(let error): diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index b382aef5b..5a8508b28 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -118,12 +118,14 @@ final class ReaderAPIAccountDelegate: AccountDelegate { switch result { case .success(let articleIDs): account.markAsRead(Set(articleIDs)) { _ in - self.refreshArticleStatus(for: account) { _ in - self.refreshProgress.completeTask() - self.refreshMissingArticles(account) { - self.refreshProgress.clear() - DispatchQueue.main.async { - completion(.success(())) + MainActor.assumeIsolated { + self.refreshArticleStatus(for: account) { _ in + self.refreshProgress.completeTask() + self.refreshMissingArticles(account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(())) + } } } } @@ -201,45 +203,48 @@ final class ReaderAPIAccountDelegate: AccountDelegate { database.selectForProcessing { result in - func processStatuses(_ syncStatuses: [SyncStatus]) { - let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } - let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } - let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } - let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } - - let group = DispatchGroup() - - group.enter() - self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { - group.leave() + MainActor.assumeIsolated { + + @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { + let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } + let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } + let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } + let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } + + let group = DispatchGroup() + + group.enter() + self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { + group.leave() + } + + group.enter() + self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { + group.leave() + } + + group.enter() + self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { + group.leave() + } + + group.enter() + self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done sending article statuses.") + completion(.success(())) + } } - - group.enter() - self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { - group.leave() + + switch result { + case .success(let syncStatuses): + processStatuses(syncStatuses) + case .failure(let databaseError): + completion(.failure(databaseError)) } - - group.enter() - self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { - group.leave() - } - - group.enter() - self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { - 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)) } } } @@ -617,10 +622,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate { self.database.insertStatuses(syncStatuses) { _ in self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { - self.sendArticleStatus(for: account) { _ in } + MainActor.assumeIsolated { + if let count = try? result.get(), count > 100 { + self.sendArticleStatus(for: account) { _ in } + } + completion(.success(())) } - completion(.success(())) } } case .failure(let error): @@ -970,18 +977,19 @@ private extension ReaderAPIAccountDelegate { switch result { case .success(let articleIDs): account.markAsRead(Set(articleIDs)) { _ in - self.refreshProgress.completeTask() - self.refreshArticleStatus(for: account) { _ in + MainActor.assumeIsolated { self.refreshProgress.completeTask() - self.refreshMissingArticles(account) { - self.refreshProgress.clear() - DispatchQueue.main.async { - completion(.success(feed)) + self.refreshArticleStatus(for: account) { _ in + self.refreshProgress.completeTask() + self.refreshMissingArticles(account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(feed)) + } + } - } } - } case .failure(let error): completion(.failure(error)) @@ -994,52 +1002,54 @@ private extension ReaderAPIAccountDelegate { func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in - func process(_ fetchedArticleIDs: Set) { - guard !fetchedArticleIDs.isEmpty else { - completion() - return - } - - os_log(.debug, log: self.log, "Refreshing missing articles...") - let group = DispatchGroup() + MainActor.assumeIsolated { + @MainActor func process(_ fetchedArticleIDs: Set) { + guard !fetchedArticleIDs.isEmpty else { + completion() + return + } - let articleIDs = Array(fetchedArticleIDs) - let chunkedArticleIDs = articleIDs.chunked(into: 150) + os_log(.debug, log: self.log, "Refreshing missing articles...") + let group = DispatchGroup() - self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1) + let articleIDs = Array(fetchedArticleIDs) + let chunkedArticleIDs = articleIDs.chunked(into: 150) - for chunk in chunkedArticleIDs { - group.enter() - self.caller.retrieveEntries(articleIDs: chunk) { result in - self.refreshProgress.completeTask() + self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1) - switch result { - case .success(let entries): - self.processEntries(account: account, entries: entries) { + for chunk in chunkedArticleIDs { + group.enter() + self.caller.retrieveEntries(articleIDs: chunk) { result in + self.refreshProgress.completeTask() + + switch result { + case .success(let entries): + self.processEntries(account: account, entries: entries) { + group.leave() + } + + case .failure(let error): + os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription) group.leave() } - - case .failure(let error): - os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription) - group.leave() } } + + group.notify(queue: DispatchQueue.main) { + self.refreshProgress.completeTask() + os_log(.debug, log: self.log, "Done refreshing missing articles.") + completion() + } } - group.notify(queue: DispatchQueue.main) { + switch articleIDsResult { + case .success(let articleIDs): + process(articleIDs) + case .failure: self.refreshProgress.completeTask() - os_log(.debug, log: self.log, "Done refreshing missing articles.") completion() } } - - switch articleIDsResult { - case .success(let articleIDs): - process(articleIDs) - case .failure: - self.refreshProgress.completeTask() - completion() - } } } @@ -1100,43 +1110,47 @@ private extension ReaderAPIAccountDelegate { database.selectPendingReadStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { - let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) - - account.fetchUnreadArticleIDs { articleIDsResult in - guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { - return - } + MainActor.assumeIsolated { + @MainActor func process(_ pendingArticleIDs: Set) { + let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) - let group = DispatchGroup() - - // Mark articles as unread - let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs) - group.enter() - account.markAsUnread(deltaUnreadArticleIDs) { _ in - group.leave() - } + account.fetchUnreadArticleIDs { articleIDsResult in - // Mark articles as read - let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs) - group.enter() - account.markAsRead(deltaReadArticleIDs) { _ in - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - completion() + MainActor.assumeIsolated { + guard let currentUnreadArticleIDs = try? articleIDsResult.get() else { + return + } + + let group = DispatchGroup() + + // Mark articles as unread + let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs) + group.enter() + account.markAsUnread(deltaUnreadArticleIDs) { _ in + group.leave() + } + + // Mark articles as read + let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs) + group.enter() + account.markAsRead(deltaReadArticleIDs) { _ in + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } } } + + switch result { + case .success(let pendingArticleIDs): + process(pendingArticleIDs) + case .failure(let error): + os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription) + } } - - switch result { - case .success(let pendingArticleIDs): - process(pendingArticleIDs) - case .failure(let error): - os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription) - } - } } @@ -1149,43 +1163,47 @@ private extension ReaderAPIAccountDelegate { database.selectPendingStarredStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { - let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) + MainActor.assumeIsolated { + @MainActor func process(_ pendingArticleIDs: Set) { + let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) - account.fetchStarredArticleIDs { articleIDsResult in - guard let currentStarredArticleIDs = try? articleIDsResult.get() else { - return - } + account.fetchStarredArticleIDs { articleIDsResult in - let group = DispatchGroup() - - // Mark articles as starred - let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs) - group.enter() - account.markAsStarred(deltaStarredArticleIDs) { _ in - group.leave() - } + MainActor.assumeIsolated { + guard let currentStarredArticleIDs = try? articleIDsResult.get() else { + return + } - // Mark articles as unstarred - let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs) - group.enter() - account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in - group.leave() - } + let group = DispatchGroup() - group.notify(queue: DispatchQueue.main) { - completion() + // Mark articles as starred + let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs) + group.enter() + account.markAsStarred(deltaStarredArticleIDs) { _ in + group.leave() + } + + // Mark articles as unstarred + let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs) + group.enter() + account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } } } - } - - switch result { - case .success(let pendingArticleIDs): - process(pendingArticleIDs) - case .failure(let error): - os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription) - } + switch result { + case .success(let pendingArticleIDs): + process(pendingArticleIDs) + case .failure(let error): + os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription) + } + } } } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index 093eab505..0332319d7 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -257,7 +257,7 @@ final class ReaderAPICaller: NSObject { } } - func deleteTag(folder: Folder, completion: @escaping (Result) -> Void) { + @MainActor func deleteTag(folder: Folder, completion: @escaping (Result) -> Void) { guard let baseURL = apiBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return diff --git a/Core/Sources/Core/DisplayNameProvider.swift b/Core/Sources/Core/DisplayNameProvider.swift index 05525a815..dbd17a78e 100644 --- a/Core/Sources/Core/DisplayNameProvider.swift +++ b/Core/Sources/Core/DisplayNameProvider.swift @@ -17,12 +17,12 @@ extension Notification.Name { public protocol DisplayNameProvider { - var nameForDisplay: String { get } + @MainActor var nameForDisplay: String { get } } public extension DisplayNameProvider { - func postDisplayNameDidChangeNotification() { + @MainActor func postDisplayNameDidChangeNotification() { NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil) } diff --git a/Core/Sources/Core/MainThreadOperation.swift b/Core/Sources/Core/MainThreadOperation.swift index a27af4952..a5f8735d6 100644 --- a/Core/Sources/Core/MainThreadOperation.swift +++ b/Core/Sources/Core/MainThreadOperation.swift @@ -39,7 +39,7 @@ public protocol MainThreadOperation: AnyObject { /// /// The completionBlock is always called on the main thread. /// The queue will clear the completionBlock after calling it. - var completionBlock: MainThreadOperationCompletionBlock? { get set } + @MainActor var completionBlock: MainThreadOperationCompletionBlock? { get set } /// Do the thing this operation does. /// diff --git a/Mac/MainWindow/Sidebar/PasteboardFeed.swift b/Mac/MainWindow/Sidebar/PasteboardFeed.swift index 696528753..6119734d0 100644 --- a/Mac/MainWindow/Sidebar/PasteboardFeed.swift +++ b/Mac/MainWindow/Sidebar/PasteboardFeed.swift @@ -153,7 +153,7 @@ extension Feed: PasteboardWriterOwner { } } -@objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting { +@MainActor @objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting { private let feed: Feed static let feedUTI = "com.ranchero.feed" diff --git a/Mac/MainWindow/Sidebar/PasteboardFolder.swift b/Mac/MainWindow/Sidebar/PasteboardFolder.swift index 85cdea238..256cb4224 100644 --- a/Mac/MainWindow/Sidebar/PasteboardFolder.swift +++ b/Mac/MainWindow/Sidebar/PasteboardFolder.swift @@ -12,8 +12,8 @@ import AppKitExtras typealias PasteboardFolderDictionary = [String: String] -struct PasteboardFolder: Hashable { - +@MainActor struct PasteboardFolder: Hashable { + private struct Key { static let name = "name" // Internal @@ -91,7 +91,7 @@ extension Folder: PasteboardWriterOwner { } } -@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting { +@MainActor @objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting { private let folder: Folder static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder" diff --git a/Mac/Scriptability/Account+Scriptability.swift b/Mac/Scriptability/Account+Scriptability.swift index 7f2d0978b..682c939f6 100644 --- a/Mac/Scriptability/Account+Scriptability.swift +++ b/Mac/Scriptability/Account+Scriptability.swift @@ -12,7 +12,7 @@ import Articles import Core @objc(ScriptableAccount) -class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { +@MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { let account:Account init (_ account:Account) { diff --git a/Mac/Scriptability/Feed+Scriptability.swift b/Mac/Scriptability/Feed+Scriptability.swift index dc64995fc..b76bad250 100644 --- a/Mac/Scriptability/Feed+Scriptability.swift +++ b/Mac/Scriptability/Feed+Scriptability.swift @@ -44,7 +44,7 @@ import Articles // I am not sure if account should prefer to be specified by name or by ID // but in either case it seems like the accountID would be used as the keydata, so I chose ID @objc(uniqueId) - var scriptingUniqueId:Any { + @MainActor var scriptingUniqueId:Any { return feed.feedID } @@ -71,7 +71,7 @@ import Articles return url } - class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed { + @MainActor class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed { let scriptableAccount = ScriptableAccount(account) if let folder = folder { let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) @@ -120,27 +120,27 @@ import Articles // MARK: --- Scriptable properties --- @objc(url) - var url:String { + @MainActor var url:String { return self.feed.url } @objc(name) - var name:String { + @MainActor var name:String { return self.feed.name ?? "" } @objc(homePageURL) - var homePageURL:String { + @MainActor var homePageURL:String { return self.feed.homePageURL ?? "" } @objc(iconURL) - var iconURL:String { + @MainActor var iconURL:String { return self.feed.iconURL ?? "" } @objc(faviconURL) - var faviconURL:String { + @MainActor var faviconURL:String { return self.feed.faviconURL ?? "" } @@ -152,13 +152,13 @@ import Articles // MARK: --- scriptable elements --- @objc(authors) - var authors:NSArray { + @MainActor var authors:NSArray { let feedAuthors = feed.authors ?? [] return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray } @objc(valueInAuthorsWithUniqueID:) - func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? { + @MainActor func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? { guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil } return ScriptableAuthor(author, container:self) } diff --git a/Mac/Scriptability/Folder+Scriptability.swift b/Mac/Scriptability/Folder+Scriptability.swift index 76bb22df6..5ebc705ec 100644 --- a/Mac/Scriptability/Folder+Scriptability.swift +++ b/Mac/Scriptability/Folder+Scriptability.swift @@ -12,7 +12,7 @@ import Articles import Core @objc(ScriptableFolder) -class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { +@MainActor class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { let folder:Folder let container:ScriptingObjectContainer diff --git a/Shared/Extensions/AddFeedDefaultContainer.swift b/Shared/Extensions/AddFeedDefaultContainer.swift index 930ddffca..7cf533368 100644 --- a/Shared/Extensions/AddFeedDefaultContainer.swift +++ b/Shared/Extensions/AddFeedDefaultContainer.swift @@ -9,7 +9,7 @@ import Foundation import Account -struct AddFeedDefaultContainer { +@MainActor struct AddFeedDefaultContainer { @MainActor static var defaultContainer: Container? { diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index 6931fa167..b2f5d2a06 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -71,7 +71,7 @@ final class FaviconDownloader { cache = [Feed: IconImage]() } - func favicon(for feed: Feed) -> IconImage? { + @MainActor func favicon(for feed: Feed) -> IconImage? { assert(Thread.isMainThread) @@ -93,7 +93,7 @@ final class FaviconDownloader { return nil } - func faviconAsIcon(for feed: Feed) -> IconImage? { + @MainActor func faviconAsIcon(for feed: Feed) -> IconImage? { if let image = cache[feed] { return image diff --git a/Shared/ShareExtension/ExtensionContainers.swift b/Shared/ShareExtension/ExtensionContainers.swift index b283f177a..896209191 100644 --- a/Shared/ShareExtension/ExtensionContainers.swift +++ b/Shared/ShareExtension/ExtensionContainers.swift @@ -37,7 +37,7 @@ struct ExtensionContainers: Codable { } -struct ExtensionAccount: ExtensionContainer { +@MainActor struct ExtensionAccount: ExtensionContainer { enum CodingKeys: String, CodingKey { case name @@ -70,7 +70,7 @@ struct ExtensionAccount: ExtensionContainer { } -struct ExtensionFolder: ExtensionContainer { +@MainActor struct ExtensionFolder: ExtensionContainer { enum CodingKeys: String, CodingKey { case accountName diff --git a/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift b/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift index 825e7f66e..8512e8b5c 100644 --- a/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift +++ b/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift @@ -9,7 +9,7 @@ import AppKit import Account -@objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting { +@MainActor @objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting { private let smartFeed: PseudoFeed diff --git a/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift b/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift index f2e948e85..930c97b18 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift @@ -124,7 +124,7 @@ public extension SyncDatabase { nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { - Task { + Task { @MainActor in do { try await self.insertStatuses(statuses) completion(nil) @@ -136,7 +136,7 @@ public extension SyncDatabase { nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) { - Task { + Task { @MainActor in do { if let syncStatuses = try await self.selectForProcessing(limit: limit) { completion(.success(Array(syncStatuses))) @@ -151,7 +151,7 @@ public extension SyncDatabase { nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) { - Task { + Task { @MainActor in do { if let count = try await self.selectPendingCount() { completion(.success(count)) @@ -167,7 +167,7 @@ public extension SyncDatabase { nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { - Task { + Task { @MainActor in do { if let articleIDs = try await self.selectPendingReadStatusArticleIDs() { completion(.success(articleIDs)) @@ -182,7 +182,7 @@ public extension SyncDatabase { nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { - Task { + Task { @MainActor in do { if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() { completion(.success(articleIDs)) @@ -197,7 +197,7 @@ public extension SyncDatabase { nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { - Task { + Task { @MainActor in do { try await self.resetAllSelectedForProcessing() completion?(nil) @@ -209,7 +209,7 @@ public extension SyncDatabase { nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { - Task { + Task { @MainActor in do { try await self.resetSelectedForProcessing(articleIDs) completion?(nil)