From f7afdfc6c4d3f94434807c308d40ace73324166b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 8 Jul 2023 15:18:57 -0700 Subject: [PATCH] Continue adopting MainActor. --- Account/Sources/Account/Account.swift | 6 +- Account/Sources/Account/AccountDelegate.swift | 2 +- Account/Sources/Account/AccountError.swift | 103 ++++++++---------- Account/Sources/Account/AccountManager.swift | 2 +- .../Sources/Account/AccountMetadataFile.swift | 2 +- Account/Sources/Account/ArticleFetcher.swift | 2 +- .../CloudKit/CloudKitAccountZone.swift | 14 +-- .../CloudKitAccountZoneDelegate.swift | 14 +-- .../CloudKit/CloudKitArticlesZone.swift | 61 ++++++----- .../CloudKitArticlesZoneDelegate.swift | 83 +++++++------- .../CloudKitSendStatusOperation.swift | 6 +- Account/Sources/Account/Container.swift | 42 +++---- .../Sources/Account/ContainerIdentifier.swift | 4 +- Account/Sources/Account/ContainerPath.swift | 2 +- Account/Sources/Account/DataExtensions.swift | 4 +- .../Account/{WebFeed.swift => Feed.swift} | 2 +- .../Sources/Account/FeedMetadataFile.swift | 2 +- .../Feedbin/FeedbinAccountDelegate.swift | 81 +++++++------- .../Feedly/FeedlyAccountDelegate.swift | 2 +- .../Feedly/FeedlyFeedContainerValidator.swift | 2 +- Account/Sources/Account/Folder.swift | 16 +-- .../LocalAccount/LocalAccountRefresher.swift | 47 ++++---- .../NewsBlurAccountDelegate+Internal.swift | 7 +- .../NewsBlur/NewsBlurAccountDelegate.swift | 10 +- Account/Sources/Account/OPMLFile.swift | 2 +- .../ReaderAPI/ReaderAPIAccountDelegate.swift | 22 ++-- .../Account/ReaderAPI/ReaderAPICaller.swift | 2 +- 27 files changed, 270 insertions(+), 272 deletions(-) rename Account/Sources/Account/{WebFeed.swift => Feed.swift} (98%) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index e1133a3d3..94c1ae043 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -61,7 +61,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 @@ -1017,13 +1017,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, // MARK: - Hashable - public func hash(into hasher: inout Hasher) { + nonisolated public func hash(into hasher: inout Hasher) { hasher.combine(accountID) } // MARK: - Equatable - public class func ==(lhs: Account, rhs: Account) -> Bool { + nonisolated public class func ==(lhs: Account, rhs: Account) -> Bool { return lhs === rhs } } diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index a8981bc6a..4233d4d6e 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 c66dc68cd..60661bf2a 100644 --- a/Account/Sources/Account/AccountError.swift +++ b/Account/Sources/Account/AccountError.swift @@ -9,30 +9,54 @@ import Foundation import RSWeb +public struct WrappedAccountError: LocalizedError { + + public let accountID: String + public let underlyingError: Error + public let isCredentialsError: Bool + + public var errorTitle: String { + NSLocalizedString("error.title.error", bundle: Bundle.module, comment: "Error") + } + + public var errorDescription: String? { + if isCredentialsError { + let localizedText = NSLocalizedString("error.message.credentials-expired.%@", bundle: Bundle.module, comment: "Your ”%@” credentials have expired.") + return String(format: localizedText, accountNameForDisplay) + } + + let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error") + return String(format: localizedText, accountNameForDisplay, underlyingError.localizedDescription) + } + + public var recoverySuggestion: String? { + if isCredentialsError { + return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials") + } + return NSLocalizedString("Please try again later.", comment: "Try later") + } + + private let accountNameForDisplay: String + + @MainActor init(account: Account, underlyingError: Error) { + self.accountID = account.accountID + self.underlyingError = underlyingError + self.accountNameForDisplay = account.nameForDisplay + + var isCredentialsError = false + if case TransportError.httpError(let status) = underlyingError { + isCredentialsError = (status == HTTPResponseCode.unauthorized || status == HTTPResponseCode.forbidden) + } + self.isCredentialsError = isCredentialsError + } +} + 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 - } else { - return nil - } - } - - public var isCredentialsError: Bool { - if case .wrappedError(let error, _) = self { - if case TransportError.httpError(let status) = error { - return isCredentialsError(status: status) - } - } - return false - } - + public var errorTitle: String { switch self { case .createErrorNotFound: @@ -41,8 +65,6 @@ public enum AccountError: LocalizedError { return NSLocalizedString("error.title.already-subscribed", bundle: Bundle.module, comment: "Already Subscribed") case .opmlImportInProgress: return NSLocalizedString("error.title.ompl-import-in-progress", bundle: Bundle.module, comment: "OPML Import in Progress") - case .wrappedError(_, _): - return NSLocalizedString("error.title.error", bundle: Bundle.module, comment: "Error") } } @@ -54,18 +76,6 @@ public enum AccountError: LocalizedError { return NSLocalizedString("error.message.feed-already-subscribed", bundle: Bundle.module, comment: "You are already subscribed to this feed and can’t add it again.") case .opmlImportInProgress: return NSLocalizedString("error.message.opml-import-in-progress", bundle: Bundle.module, comment: "An OPML import for this account is already running.") - case .wrappedError(let error, let account): - switch error { - case TransportError.httpError(let status): - if isCredentialsError(status: status) { - let localizedText = NSLocalizedString("error.message.credentials-expired.%@", bundle: Bundle.module, comment: "Your ”%@” credentials have expired.") - return String(format: localizedText, account.nameForDisplay) - } else { - return unknownError(error, account) - } - default: - return unknownError(error, account) - } } } @@ -75,35 +85,8 @@ public enum AccountError: LocalizedError { return nil case .createErrorAlreadySubscribed: return nil - case .wrappedError(let error, _): - switch error { - case TransportError.httpError(let status): - if isCredentialsError(status: status) { - return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials") - } else { - return NSLocalizedString("Please try again later.", comment: "Try later") - } - default: - return NSLocalizedString("Please try again later.", comment: "Try later") - } default: return NSLocalizedString("Please try again later.", comment: "Try later") } } - -} - -// MARK: Private - -private extension AccountError { - - func unknownError(_ error: Error, _ account: Account) -> 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 - } - - func isCredentialsError(status: Int) -> Bool { - return status == 401 || status == 403 - } - } diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index c0f20db11..a6d488e80 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -15,7 +15,7 @@ import RSDatabase // Main thread only. -public final class AccountManager: UnreadCountProvider { +@MainActor public final class AccountManager: UnreadCountProvider { public static var shared: AccountManager! public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml" diff --git a/Account/Sources/Account/AccountMetadataFile.swift b/Account/Sources/Account/AccountMetadataFile.swift index 644f7984a..4c9fcb14b 100644 --- a/Account/Sources/Account/AccountMetadataFile.swift +++ b/Account/Sources/Account/AccountMetadataFile.swift @@ -9,7 +9,7 @@ import Foundation import RSCore -final class AccountMetadataFile: Logging { +@MainActor final class AccountMetadataFile: Logging { private let fileURL: URL private let account: Account diff --git a/Account/Sources/Account/ArticleFetcher.swift b/Account/Sources/Account/ArticleFetcher.swift index 91478cb95..6e8e6e5f6 100644 --- a/Account/Sources/Account/ArticleFetcher.swift +++ b/Account/Sources/Account/ArticleFetcher.swift @@ -10,7 +10,7 @@ import Foundation import Articles import ArticlesDatabase -public protocol ArticleFetcher { +@MainActor public protocol ArticleFetcher { func fetchArticles() throws -> Set
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index f18fa163e..c637511c2 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -88,7 +88,7 @@ final class CloudKitAccountZone: CloudKitZone { } /// Persist a web feed record to iCloud and return the external key - func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result) -> Void) { + @MainActor func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result) -> Void) { let recordID = CKRecord.ID(recordName: url.md5String, zoneID: zoneID) let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID) record[CloudKitFeed.Fields.url] = url @@ -138,7 +138,7 @@ final class CloudKitAccountZone: CloudKitZone { } /// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted - func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result) -> Void) { + @MainActor func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result) -> Void) { guard let fromContainerExternalID = from.externalID else { completion(.failure(CloudKitZoneError.corruptAccount)) return @@ -187,7 +187,7 @@ final class CloudKitAccountZone: CloudKitZone { } } - func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { + @MainActor func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else { completion(.failure(CloudKitZoneError.corruptAccount)) return @@ -209,7 +209,7 @@ final class CloudKitAccountZone: CloudKitZone { } } - func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result) -> Void) { + @MainActor func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result) -> Void) { guard let toContainerExternalID = to.externalID else { completion(.failure(CloudKitZoneError.corruptAccount)) return @@ -230,7 +230,7 @@ final class CloudKitAccountZone: CloudKitZone { } } - func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) { + @MainActor func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) { guard let folderExternalID = folder.externalID else { completion(.failure(CloudKitAccountZoneError.unknown)) return @@ -292,7 +292,7 @@ final class CloudKitAccountZone: CloudKitZone { createContainer(name: name, isAccount: false, completion: completion) } - func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result) -> Void) { + @MainActor func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result) -> Void) { guard let externalID = folder.externalID else { completion(.failure(CloudKitZoneError.corruptAccount)) return @@ -312,7 +312,7 @@ final class CloudKitAccountZone: CloudKitZone { } } - func removeFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { + @MainActor func removeFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { delete(externalID: folder.externalID, completion: completion) } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift index e9550223b..0ba754ad5 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -31,7 +31,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { self.articlesZone = articlesZone } - func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { + @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { for deletedRecordKey in deleted { switch deletedRecordKey.recordType { case CloudKitAccountZone.CloudKitFeed.recordType: @@ -57,7 +57,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { completion(.success(())) } - func addOrUpdateFeed(_ record: CKRecord) { + @MainActor func addOrUpdateFeed(_ record: CKRecord) { guard let account = account, let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String, let containerExternalIDs = record[CloudKitAccountZone.CloudKitFeed.Fields.containerExternalIDs] as? [String], @@ -82,7 +82,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { } } - func removeFeed(_ externalID: String) { + @MainActor func removeFeed(_ externalID: String) { if let feed = account?.existingFeed(withExternalID: externalID), let containers = account?.existingContainers(withFeed: feed) { containers.forEach { feed.dropConditionalGetInfo() @@ -91,7 +91,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { } } - func addOrUpdateContainer(_ record: CKRecord) { + @MainActor func addOrUpdateContainer(_ record: CKRecord) { guard let account = account, let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String, let isAccount = record[CloudKitAccountZone.CloudKitContainer.Fields.isAccount] as? String, @@ -130,7 +130,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { } } - func removeContainer(_ externalID: String) { + @MainActor func removeContainer(_ externalID: String) { if let folder = account?.existingFolder(withExternalID: externalID) { account?.removeFolder(folder) } @@ -140,7 +140,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { private extension CloudKitAcountZoneDelegate { - func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { + @MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { guard let account = account else { return } feed.name = name @@ -168,7 +168,7 @@ private extension CloudKitAcountZoneDelegate { } } - func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) { + @MainActor func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) { guard let account = account else { return } if account.existingFeed(withExternalID: feedExternalID) != nil { diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index 5408aca8c..a565ab9c8 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -62,19 +62,22 @@ final class CloudKitArticlesZone: CloudKitZone { migrateChangeToken() } - func saveNewArticles(_ articles: Set
, completion: @escaping ((Result) -> Void)) { - guard !articles.isEmpty else { - completion(.success(())) - return - } - - var records = [CKRecord]() - - let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true } - for saveArticle in saveArticles { - records.append(makeStatusRecord(saveArticle)) - records.append(makeArticleRecord(saveArticle)) - } + @MainActor func saveNewArticles(_ articles: Set
, completion: @escaping ((Result) -> Void)) { + guard !articles.isEmpty else { + completion(.success(())) + return + } + + let records: [CKRecord] = { + var recordsAccumulator = [CKRecord]() + + let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true } + for saveArticle in saveArticles { + recordsAccumulator.append(makeStatusRecord(saveArticle)) + recordsAccumulator.append(makeArticleRecord(saveArticle)) + } + return recordsAccumulator + }() compressionQueue.async { let compressedRecords = self.compressArticleRecords(records) @@ -88,7 +91,7 @@ final class CloudKitArticlesZone: CloudKitZone { delete(ckQuery: ckQuery, completion: completion) } - func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { + @MainActor func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { guard !statusUpdates.isEmpty else { completion(.success(())) return @@ -114,12 +117,16 @@ final class CloudKitArticlesZone: CloudKitZone { } } + let modifyRecordsCopy = modifyRecords + let newRecordsCopy = newRecords + let deleteRecordIDsCopy = deleteRecordIDs + compressionQueue.async { - let compressedModifyRecords = self.compressArticleRecords(modifyRecords) - self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in + let compressedModifyRecords = self.compressArticleRecords(modifyRecordsCopy) + self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDsCopy) { result in switch result { case .success: - let compressedNewRecords = self.compressArticleRecords(newRecords) + let compressedNewRecords = self.compressArticleRecords(newRecordsCopy) self.saveIfNew(compressedNewRecords) { result in switch result { case .success: @@ -143,12 +150,14 @@ private extension CloudKitArticlesZone { func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { if case CloudKitZoneError.userDeletedZone = error { self.createZoneRecord() { result in - switch result { - case .success: - self.modifyArticles(statusUpdates, completion: completion) - case .failure(let error): - completion(.failure(error)) - } + Task { @MainActor in + switch result { + case .success: + self.modifyArticles(statusUpdates, completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } } } else { completion(.failure(error)) @@ -163,7 +172,7 @@ private extension CloudKitArticlesZone { return "a|\(id)" } - func makeStatusRecord(_ article: Article) -> CKRecord { + @MainActor func makeStatusRecord(_ article: Article) -> CKRecord { let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID) let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID) if let feedExternalID = article.feed?.externalID { @@ -174,7 +183,7 @@ private extension CloudKitArticlesZone { return record } - func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord { + @MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord { let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID) let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID) @@ -188,7 +197,7 @@ private extension CloudKitArticlesZone { return record } - func makeArticleRecord(_ article: Article) -> CKRecord { + @MainActor func makeArticleRecord(_ article: Article) -> CKRecord { let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID) let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID) diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift index e7d26d9f8..d019fa260 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -29,38 +29,37 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging { } func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { - - database.selectPendingReadStatusArticleIDs() { result in - switch result { - case .success(let pendingReadStatusArticleIDs): - self.database.selectPendingStarredStatusArticleIDs() { result in - switch result { - case .success(let pendingStarredStatusArticleIDs): - self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in - if let error = error { - completion(.failure(error)) - } else { - self.update(records: updated, - pendingReadStatusArticleIDs: pendingReadStatusArticleIDs, - pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs, - completion: completion) - } - } - case .failure(let error): + database.selectPendingReadStatusArticleIDs() { result in + switch result { + case .success(let pendingReadStatusArticleIDs): + + self.database.selectPendingStarredStatusArticleIDs() { result in + switch result { + case .success(let pendingStarredStatusArticleIDs): + self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in + Task { @MainActor in + if let error = error { + completion(.failure(error)) + } else { + self.update(records: updated, + pendingReadStatusArticleIDs: pendingReadStatusArticleIDs, + pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs, + completion: completion) + } + } + } + case .failure(let error): self.logger.error("Error occurred getting pending starred records: \(error.localizedDescription, privacy: .public)") - completion(.failure(CloudKitZoneError.unknown)) - } - } - case .failure(let error): + completion(.failure(CloudKitZoneError.unknown)) + } + } + case .failure(let error): self.logger.error("Error occurred getting pending read status records: \(error.localizedDescription, privacy: .public)") - completion(.failure(CloudKitZoneError.unknown)) - } - - } - - } - + completion(.failure(CloudKitZoneError.unknown)) + } + } + } } private extension CloudKitArticlesZoneDelegate { @@ -76,21 +75,23 @@ private extension CloudKitArticlesZoneDelegate { } database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { databaseError in - if let databaseError = databaseError { - completion(databaseError) - } else { - self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in - if let databaseError = databaseError { - completion(databaseError) - } else { - completion(nil) - } - } - } + Task { @MainActor in + if let databaseError = databaseError { + completion(databaseError) + } else { + self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in + if let databaseError = databaseError { + completion(databaseError) + } else { + completion(nil) + } + } + } + } } } - func update(records: [CKRecord], pendingReadStatusArticleIDs: Set, pendingStarredStatusArticleIDs: Set, completion: @escaping (Result) -> Void) { + @MainActor func update(records: [CKRecord], pendingReadStatusArticleIDs: Set, pendingStarredStatusArticleIDs: Set, completion: @escaping (Result) -> Void) { let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ stripPrefix($0.externalID) })) let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ stripPrefix($0.externalID) })) diff --git a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift index e4a4cb593..5579018c9 100644 --- a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -71,7 +71,7 @@ private extension CloudKitSendStatusOperation { switch result { case .success(let syncStatuses): - func stopProcessing() { + @MainActor func stopProcessing() { if self.showProgress { self.refreshProgress?.completeTask() } @@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation { let articleIDs = syncStatuses.map({ $0.articleID }) account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in - func processWithArticles(_ articles: Set
) { + @MainActor func processWithArticles(_ articles: Set
) { let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID }) let articlesDict = articles.reduce(into: [String: Article]()) { result, article in @@ -118,7 +118,7 @@ private extension CloudKitSendStatusOperation { return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key]) } - func done(_ stop: Bool) { + func done(_ stop: Bool) { // Don't clear the last one since we might have had additional ticks added if self.showProgress && self.refreshProgress?.numberRemaining ?? 0 > 1 { self.refreshProgress?.completeTask() diff --git a/Account/Sources/Account/Container.swift b/Account/Sources/Account/Container.swift index a5d1714db..8017ec072 100644 --- a/Account/Sources/Account/Container.swift +++ b/Account/Sources/Account/Container.swift @@ -18,35 +18,35 @@ extension Notification.Name { public protocol Container: AnyObject, ContainerIdentifiable { - var account: Account? { get } - var topLevelFeeds: Set { get set } - var folders: Set? { get set } - var externalID: String? { get set } + @MainActor var account: Account? { get } + @MainActor var topLevelFeeds: Set { get set } + @MainActor var folders: Set? { get set } + @MainActor var externalID: String? { get set } - func hasAtLeastOneFeed() -> Bool - func objectIsChild(_ object: AnyObject) -> Bool + @MainActor func hasAtLeastOneFeed() -> Bool + @MainActor func objectIsChild(_ object: AnyObject) -> Bool - func hasChildFolder(with: String) -> Bool - func childFolder(with: String) -> Folder? + @MainActor func hasChildFolder(with: String) -> Bool + @MainActor func childFolder(with: String) -> Folder? - func removeFeed(_ feed: Feed) - func addFeed(_ feed: Feed) + @MainActor func removeFeed(_ feed: Feed) + @MainActor func addFeed(_ feed: Feed) //Recursive — checks subfolders - func flattenedFeeds() -> Set - func has(_ feed: Feed) -> Bool - func hasFeed(with feedID: String) -> Bool - func hasFeed(withURL url: String) -> Bool - func existingFeed(withFeedID: String) -> Feed? - func existingFeed(withURL url: String) -> Feed? - func existingFeed(withExternalID externalID: String) -> Feed? - func existingFolder(with name: String) -> Folder? - func existingFolder(withID: Int) -> Folder? + @MainActor func flattenedFeeds() -> Set + @MainActor func has(_ feed: Feed) -> Bool + @MainActor func hasFeed(with feedID: String) -> Bool + @MainActor func hasFeed(withURL url: String) -> Bool + @MainActor func existingFeed(withFeedID: String) -> Feed? + @MainActor func existingFeed(withURL url: String) -> Feed? + @MainActor func existingFeed(withExternalID externalID: String) -> Feed? + @MainActor func existingFolder(with name: String) -> Folder? + @MainActor func existingFolder(withID: Int) -> Folder? - func postChildrenDidChangeNotification() + @MainActor func postChildrenDidChangeNotification() } -public extension Container { +@MainActor public extension Container { func hasAtLeastOneFeed() -> Bool { return topLevelFeeds.count > 0 diff --git a/Account/Sources/Account/ContainerIdentifier.swift b/Account/Sources/Account/ContainerIdentifier.swift index 0db60e6d4..b731209e8 100644 --- a/Account/Sources/Account/ContainerIdentifier.swift +++ b/Account/Sources/Account/ContainerIdentifier.swift @@ -9,10 +9,10 @@ import Foundation public protocol ContainerIdentifiable { - var containerID: ContainerIdentifier? { get } + @MainActor var containerID: ContainerIdentifier? { get } } -public enum ContainerIdentifier: Hashable, Equatable { +@MainActor public enum ContainerIdentifier: Hashable, Equatable { case smartFeedController case account(String) // accountID case folder(String, String) // accountID, folderName 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/DataExtensions.swift b/Account/Sources/Account/DataExtensions.swift index a503ea62d..79f5a0227 100644 --- a/Account/Sources/Account/DataExtensions.swift +++ b/Account/Sources/Account/DataExtensions.swift @@ -48,7 +48,7 @@ extension Feed { public extension Article { - var account: Account? { + @MainActor var account: Account? { // The force unwrapped shared instance was crashing Account.framework unit tests. guard let manager = AccountManager.shared else { return nil @@ -56,7 +56,7 @@ public extension Article { return manager.existingAccount(with: accountID) } - var feed: Feed? { + @MainActor var feed: Feed? { return account?.existingFeed(withFeedID: feedID) } } diff --git a/Account/Sources/Account/WebFeed.swift b/Account/Sources/Account/Feed.swift similarity index 98% rename from Account/Sources/Account/WebFeed.swift rename to Account/Sources/Account/Feed.swift index 233fa833d..d0609a552 100644 --- a/Account/Sources/Account/WebFeed.swift +++ b/Account/Sources/Account/Feed.swift @@ -210,7 +210,7 @@ public final class Feed: FeedProtocol, Renamable, Hashable, ObservableObject { // MARK: - Renamable - public func rename(to newName: String, completion: @escaping (Result) -> Void) { + @MainActor public func rename(to newName: String, completion: @escaping (Result) -> Void) { guard let account = account else { return } account.renameFeed(self, to: newName, completion: completion) } diff --git a/Account/Sources/Account/FeedMetadataFile.swift b/Account/Sources/Account/FeedMetadataFile.swift index 7363e74a7..4ca8231ce 100644 --- a/Account/Sources/Account/FeedMetadataFile.swift +++ b/Account/Sources/Account/FeedMetadataFile.swift @@ -9,7 +9,7 @@ import Foundation import RSCore -final class FeedMetadataFile: Logging { +@MainActor final class FeedMetadataFile: Logging { private let fileURL: URL private let account: Account diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift index c25ff5a57..9f01c59ff 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift @@ -19,7 +19,7 @@ public enum FeedbinAccountDelegateError: String, Error { case unknown = "An unknown error occurred." } -final class FeedbinAccountDelegate: AccountDelegate, Logging { +@MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging { private let database: SyncDatabase @@ -92,7 +92,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -101,7 +101,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -134,7 +134,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { database.selectForProcessing { result in - func processStatuses(_ syncStatuses: [SyncStatus]) { + @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 } @@ -280,7 +280,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { self.refreshProgress.completeTask() self.isOPMLImportInProgress = false DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -315,7 +315,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -409,7 +409,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -437,7 +437,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -484,7 +484,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -1163,43 +1163,46 @@ private extension FeedbinAccountDelegate { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in - func process(_ fetchedArticleIDs: Set) { + @MainActor func process(_ fetchedArticleIDs: Set) { let group = DispatchGroup() var errorOccurred = false let articleIDs = Array(fetchedArticleIDs) let chunkedArticleIDs = articleIDs.chunked(into: 100) - for chunk in chunkedArticleIDs { - group.enter() - self.caller.retrieveEntries(articleIDs: chunk) { result in + for chunk in chunkedArticleIDs { + group.enter() + self.caller.retrieveEntries(articleIDs: chunk) { result in - switch result { - case .success(let entries): +// Task { @MainActor in + switch result { + case .success(let entries): + self.processEntries(account: account, entries: entries) { error in + group.leave() + if error != nil { + errorOccurred = true + } + } - self.processEntries(account: account, entries: entries) { error in - group.leave() - if error != nil { - errorOccurred = true - } - } - - case .failure(let error): - errorOccurred = true - self.logger.error("Refreshing missing articles failed: \(error.localizedDescription, privacy: .public)") - group.leave() - } - } - } + case .failure(let error): + errorOccurred = true + self.logger.error("Refreshing missing articles failed: \(error.localizedDescription, privacy: .public)") + group.leave() + } +// } + } + } group.notify(queue: DispatchQueue.main) { - self.refreshProgress.completeTask() - self.logger.debug("Done refreshing missing articles.") - if errorOccurred { - completion(.failure(FeedbinAccountDelegateError.unknown)) - } else { - completion(.success(())) - } +// Task { @MainActor in + self.refreshProgress.completeTask() + self.logger.debug("Done refreshing missing articles.") + if errorOccurred { + completion(.failure(FeedbinAccountDelegateError.unknown)) + } else { + completion(.success(())) + } +// } } } @@ -1273,7 +1276,7 @@ private extension FeedbinAccountDelegate { database.selectPendingReadStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { + @MainActor func process(_ pendingArticleIDs: Set) { let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } ) let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs) @@ -1326,7 +1329,7 @@ private extension FeedbinAccountDelegate { database.selectPendingStarredStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { + @MainActor func process(_ pendingArticleIDs: Set) { let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } ) let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs) @@ -1386,7 +1389,7 @@ private extension FeedbinAccountDelegate { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index c62786325..0fc358a5a 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -236,7 +236,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { self.refreshProgress.completeTask() self.isOPMLImportInProgress = false DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } diff --git a/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift b/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift index da4e6fb81..f01e59bf1 100644 --- a/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift +++ b/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift @@ -11,7 +11,7 @@ import Foundation struct FeedlyFeedContainerValidator { var container: Container - func getValidContainer() throws -> (Folder, String) { + @MainActor func getValidContainer() throws -> (Folder, String) { guard let folder = container as? Folder else { throw FeedlyAccountDelegateError.addFeedChooseFolder } diff --git a/Account/Sources/Account/Folder.swift b/Account/Sources/Account/Folder.swift index afe52dcc4..a83c44a19 100644 --- a/Account/Sources/Account/Folder.swift +++ b/Account/Sources/Account/Folder.swift @@ -16,7 +16,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { return .read } - public var containerID: ContainerIdentifier? { + @MainActor public var containerID: ContainerIdentifier? { guard let accountID = account?.accountID else { assertionFailure("Expected feed.account, but got nil.") return nil @@ -24,7 +24,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { return ContainerIdentifier.folder(accountID, nameForDisplay) } - public var itemID: ItemIdentifier? { + @MainActor public var itemID: ItemIdentifier? { guard let accountID = account?.accountID else { assertionFailure("Expected feed.account, but got nil.") return nil @@ -66,7 +66,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { // MARK: - Renamable - public func rename(to name: String, completion: @escaping (Result) -> Void) { + @MainActor public func rename(to name: String, completion: @escaping (Result) -> Void) { guard let account = account else { return } account.renameFolder(self, to: name, completion: completion) } @@ -122,7 +122,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { postChildrenDidChangeNotification() } - public func addFeeds(_ feeds: Set) { + @MainActor public func addFeeds(_ feeds: Set) { guard !feeds.isEmpty else { return } @@ -135,7 +135,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { postChildrenDidChangeNotification() } - public func removeFeeds(_ feeds: Set) { + @MainActor public func removeFeeds(_ feeds: Set) { guard !feeds.isEmpty else { return } @@ -168,7 +168,7 @@ private extension Folder { unreadCount = updatedUnreadCount } - func childrenContain(_ feed: Feed) -> Bool { + @MainActor func childrenContain(_ feed: Feed) -> Bool { return topLevelFeeds.contains(feed) } } @@ -177,7 +177,7 @@ private extension Folder { extension Folder: OPMLRepresentable { - public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String { + @MainActor public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String { let attrExternalID: String = { if allowCustomAttributes, let externalID = externalID { @@ -220,7 +220,7 @@ extension Folder: OPMLRepresentable { // MARK: Set -extension Set where Element == Folder { +@MainActor extension Set where Element == Folder { func sorted() -> Array { return sorted(by: { (folder1, folder2) -> Bool in diff --git a/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift b/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift index 20af6aee4..73400d2d4 100644 --- a/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift +++ b/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift @@ -93,29 +93,30 @@ extension LocalAccountRefresher: DownloadSessionDelegate { let parserData = ParserData(url: feed.url, data: data) FeedParser.parse(parserData) { (parsedFeed, error) in - - guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else { - completion() - self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) - return - } - - 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) { - completion() - } - } else { - completion() - self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) - } - } - + + Task { @MainActor in + guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else { + completion() + self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) + return + } + + 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) { + completion() + } + } 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 f85cec12c..4377f4b74 100644 --- a/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift +++ b/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift @@ -323,7 +323,7 @@ extension NewsBlurAccountDelegate { } database.selectPendingReadStatusArticleIDs() { result in - func process(_ pendingStoryHashes: Set) { + @MainActor func process(_ pendingStoryHashes: Set) { let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } ) let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes) @@ -371,7 +371,8 @@ extension NewsBlurAccountDelegate { } database.selectPendingStarredStatusArticleIDs() { result in - func process(_ pendingStoryHashes: Set) { + + @MainActor func process(_ pendingStoryHashes: Set) { let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } ) let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes) @@ -550,7 +551,7 @@ extension NewsBlurAccountDelegate { case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift index 007582d70..adde36f7b 100644 --- a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift @@ -90,7 +90,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -134,7 +134,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { logger.debug("Sending story statuses...") database.selectForProcessing { result in - func processStatuses(_ syncStatuses: [SyncStatus]) { + @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } @@ -270,7 +270,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in - func process(_ fetchedHashes: Set) { + @MainActor func process(_ fetchedHashes: Set) { let group = DispatchGroup() var errorOccurred = false @@ -432,7 +432,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { self.createFeed(account: account, newsBlurFeed: feed, name: name, container: container, completion: completion) case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -459,7 +459,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } diff --git a/Account/Sources/Account/OPMLFile.swift b/Account/Sources/Account/OPMLFile.swift index a5558c138..76c4aa7f0 100644 --- a/Account/Sources/Account/OPMLFile.swift +++ b/Account/Sources/Account/OPMLFile.swift @@ -10,7 +10,7 @@ import Foundation import RSCore import RSParser -final class OPMLFile: Logging { +@MainActor final class OPMLFile: Logging { private let fileURL: URL private let account: Account diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 9fdd97692..00afe4015 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -33,7 +33,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } -final class ReaderAPIAccountDelegate: AccountDelegate, Logging { +@MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging { private let variant: ReaderAPIVariant @@ -135,7 +135,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { DispatchQueue.main.async { self.refreshProgress.clear() - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) if wrappedError.isCredentialsError, let basicCredentials = try? account.retrieveCredentials(type: .readerBasic), let endpoint = account.endpointURL { self.caller.credentials = basicCredentials @@ -198,7 +198,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { database.selectForProcessing { result in - func processStatuses(_ syncStatuses: [SyncStatus]) { + @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 } @@ -312,7 +312,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -422,7 +422,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -456,7 +456,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -487,7 +487,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -537,7 +537,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging { } case .failure(let error): DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) + let wrappedError = WrappedAccountError(account: account, underlyingError: error) completion(.failure(wrappedError)) } } @@ -980,7 +980,7 @@ private extension ReaderAPIAccountDelegate { func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in - func process(_ fetchedArticleIDs: Set) { + @MainActor func process(_ fetchedArticleIDs: Set) { guard !fetchedArticleIDs.isEmpty else { completion() return @@ -1086,7 +1086,7 @@ private extension ReaderAPIAccountDelegate { database.selectPendingReadStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { + @MainActor func process(_ pendingArticleIDs: Set) { let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) account.fetchUnreadArticleIDs { articleIDsResult in @@ -1135,7 +1135,7 @@ private extension ReaderAPIAccountDelegate { database.selectPendingStarredStatusArticleIDs() { result in - func process(_ pendingArticleIDs: Set) { + @MainActor func process(_ pendingArticleIDs: Set) { let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs) account.fetchStarredArticleIDs { articleIDsResult in diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index fd2995c25..cd05b27ca 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -255,7 +255,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