From 177d660cffcce1aa37dda768bc067b3c36242947 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 23 Mar 2024 12:20:32 -0700 Subject: [PATCH] Fix numerous concurrency warnings. --- Account/Sources/Account/Account.swift | 43 ++++++++++++------- Account/Sources/Account/AccountManager.swift | 12 +++--- .../Sources/Account/AccountMetadataFile.swift | 4 +- .../Sources/Account/FeedMetadataFile.swift | 10 ++--- Account/Sources/Account/OPMLFile.swift | 4 +- Core/Sources/Core/CoalescingQueue.swift | 4 +- Shared/Favicons/FaviconDownloader.swift | 8 +++- Shared/Images/FeedIconDownloader.swift | 42 ++++++++++-------- .../ExtensionContainersFile.swift | 8 ++-- Shared/SmartFeeds/SmartFeed.swift | 4 +- 10 files changed, 81 insertions(+), 58 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 823de932d..b77d931a3 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -209,15 +209,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, let dataFolder: String let database: ArticlesDatabase var delegate: AccountDelegate - static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) + @MainActor static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) private var unreadCounts = [String: Int]() // [feedID: Int] private var _flattenedFeeds = Set() private var flattenedFeedsNeedUpdate = true - private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self) - private lazy var metadataFile = AccountMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("Settings.plist"), account: self) + @MainActor private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self) + @MainActor private lazy var metadataFile = AccountMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("Settings.plist"), account: self) var metadata = AccountMetadata() { didSet { delegate.accountMetadata = metadata @@ -248,7 +248,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } else { NotificationCenter.default.post(name: .AccountRefreshDidFinish, object: self) - opmlFile.markAsDirty() + Task { @MainActor in + opmlFile.markAsDirty() + } } } } @@ -258,7 +260,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return delegate.refreshProgress } - init(dataFolder: String, type: AccountType, accountID: String, secretsProvider: SecretsProvider, transport: Transport? = nil) { + @MainActor init(dataFolder: String, type: AccountType, accountID: String, secretsProvider: SecretsProvider, transport: Transport? = nil) { switch type { case .onMyMac: self.delegate = LocalAccountDelegate() @@ -480,9 +482,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func save() { - metadataFile.save() - feedMetadataFile.save() - opmlFile.save() + Task { @MainActor in + metadataFile.save() + feedMetadataFile.save() + opmlFile.save() + } } public func prepareForDeletion() { @@ -775,9 +779,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func structureDidChange() { // Feeds were added or deleted. Or folders added or deleted. // Or feeds inside folders were added or deleted. - opmlFile.markAsDirty() - flattenedFeedsNeedUpdate = true - feedDictionariesNeedUpdate = true + Task { @MainActor in + opmlFile.markAsDirty() + flattenedFeedsNeedUpdate = true + feedDictionariesNeedUpdate = true + } } func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) { @@ -1059,7 +1065,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, extension Account: AccountMetadataDelegate { func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) { - metadataFile.markAsDirty() + Task { @MainActor in + metadataFile.markAsDirty() + } } } @@ -1068,11 +1076,14 @@ extension Account: AccountMetadataDelegate { extension Account: FeedMetadataDelegate { func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) { - feedMetadataFile.markAsDirty() - guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else { - return + + Task { @MainActor in + feedMetadataFile.markAsDirty() + guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else { + return + } + feed.postFeedSettingDidChangeNotification(key) } - feed.postFeedSettingDidChangeNotification(key) } } diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 18736c97e..a2d93e6f1 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -96,8 +96,8 @@ public final class AccountManager: UnreadCountProvider { return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray) } - public init(accountsFolder: String, secretsProvider: SecretsProvider) { - + @MainActor public init(accountsFolder: String, secretsProvider: SecretsProvider) { + self.accountsFolder = accountsFolder self.secretsProvider = secretsProvider @@ -127,7 +127,7 @@ public final class AccountManager: UnreadCountProvider { // MARK: - API - public func createAccount(type: AccountType) -> Account { + @MainActor public func createAccount(type: AccountType) -> Account { let accountID = type == .cloudKit ? "iCloud" : UUID().uuidString let accountFolder = (accountsFolder as NSString).appendingPathComponent("\(type.rawValue)_\(accountID)") @@ -438,11 +438,11 @@ private extension AccountManager { unreadCount = calculateUnreadCount(activeAccounts) } - func loadAccount(_ accountSpecifier: AccountSpecifier) -> Account? { + @MainActor func loadAccount(_ accountSpecifier: AccountSpecifier) -> Account? { return Account(dataFolder: accountSpecifier.folderPath, type: accountSpecifier.type, accountID: accountSpecifier.identifier, secretsProvider: secretsProvider) } - func loadAccount(_ filename: String) -> Account? { + @MainActor func loadAccount(_ filename: String) -> Account? { let folderPath = (accountsFolder as NSString).appendingPathComponent(filename) if let accountSpecifier = AccountSpecifier(folderPath: folderPath) { return loadAccount(accountSpecifier) @@ -450,7 +450,7 @@ private extension AccountManager { return nil } - func readAccountsFromDisk() { + @MainActor func readAccountsFromDisk() { var filenames: [String]? do { diff --git a/Account/Sources/Account/AccountMetadataFile.swift b/Account/Sources/Account/AccountMetadataFile.swift index 6cd8014ce..accd58628 100644 --- a/Account/Sources/Account/AccountMetadataFile.swift +++ b/Account/Sources/Account/AccountMetadataFile.swift @@ -10,8 +10,8 @@ import Foundation import os.log import Core -final class AccountMetadataFile { - +@MainActor final class AccountMetadataFile { + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "accountMetadataFile") private let fileURL: URL diff --git a/Account/Sources/Account/FeedMetadataFile.swift b/Account/Sources/Account/FeedMetadataFile.swift index acbbbb7eb..db70a619e 100644 --- a/Account/Sources/Account/FeedMetadataFile.swift +++ b/Account/Sources/Account/FeedMetadataFile.swift @@ -17,19 +17,19 @@ final class FeedMetadataFile { private let fileURL: URL private let account: Account - private var isDirty = false { + @MainActor private var isDirty = false { didSet { queueSaveToDiskIfNeeded() } } - private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5) + @MainActor private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5) init(filename: String, account: Account) { self.fileURL = URL(fileURLWithPath: filename) self.account = account } - func markAsDirty() { + @MainActor func markAsDirty() { isDirty = true } @@ -61,11 +61,11 @@ final class FeedMetadataFile { private extension FeedMetadataFile { - func queueSaveToDiskIfNeeded() { + @MainActor func queueSaveToDiskIfNeeded() { saveQueue.add(self, #selector(saveToDiskIfNeeded)) } - @objc func saveToDiskIfNeeded() { + @MainActor @objc func saveToDiskIfNeeded() { if isDirty { isDirty = false save() diff --git a/Account/Sources/Account/OPMLFile.swift b/Account/Sources/Account/OPMLFile.swift index 8bac59063..1c196b743 100644 --- a/Account/Sources/Account/OPMLFile.swift +++ b/Account/Sources/Account/OPMLFile.swift @@ -11,8 +11,8 @@ import os.log import RSParser import Core -final class OPMLFile { - +@MainActor final class OPMLFile { + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "opmlFile") private let fileURL: URL diff --git a/Core/Sources/Core/CoalescingQueue.swift b/Core/Sources/Core/CoalescingQueue.swift index ec2271fe0..94c7f5de0 100644 --- a/Core/Sources/Core/CoalescingQueue.swift +++ b/Core/Sources/Core/CoalescingQueue.swift @@ -30,9 +30,9 @@ struct QueueCall: Equatable { } } -@objc public final class CoalescingQueue: NSObject { +@MainActor @objc public final class CoalescingQueue: NSObject { - public static let standard = CoalescingQueue(name: "Standard", interval: 0.05, maxInterval: 0.1) + @MainActor public static let standard = CoalescingQueue(name: "Standard", interval: 0.05, maxInterval: 0.1) public let name: String public var isPaused = false private let interval: TimeInterval diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index 9f538cebf..6931fa167 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -278,11 +278,15 @@ private extension FaviconDownloader { } func queueSaveHomePageToFaviconURLCacheIfNeeded() { - FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded)) + Task { @MainActor in + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded)) + } } func queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() { - FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded)) + Task { @MainActor in + FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded)) + } } func saveHomePageToFaviconURLCache() { diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index ae9a3444a..874c497ce 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -18,7 +18,7 @@ extension Notification.Name { static let FeedIconDidBecomeAvailable = Notification.Name("FeedIconDidBecomeAvailableNotification") // UserInfoKey.feed } -public final class FeedIconDownloader { +@MainActor public final class FeedIconDownloader { private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) @@ -81,14 +81,16 @@ public final class FeedIconDownloader { return IconImage.appIcon } - func checkHomePageURL() { + @MainActor func checkHomePageURL() { guard let homePageURL = feed.homePageURL else { return } icon(forHomePageURL: homePageURL, feed: feed) { (image) in - if let image = image { - self.postFeedIconDidBecomeAvailableNotification(feed) - self.cache[feed] = IconImage(image) + Task { @MainActor in + if let image = image { + self.postFeedIconDidBecomeAvailableNotification(feed) + self.cache[feed] = IconImage(image) + } } } } @@ -96,11 +98,13 @@ public final class FeedIconDownloader { func checkFeedIconURL() { if let iconURL = feed.iconURL { icon(forURL: iconURL, feed: feed) { (image) in - if let image = image { - self.postFeedIconDidBecomeAvailableNotification(feed) - self.cache[feed] = IconImage(image) - } else { - checkHomePageURL() + Task { @MainActor in + if let image = image { + self.postFeedIconDidBecomeAvailableNotification(feed) + self.cache[feed] = IconImage(image) + } else { + checkHomePageURL() + } } } } else { @@ -110,9 +114,11 @@ public final class FeedIconDownloader { if let feedProviderURL = feedURLToIconURLCache[feed.url] { self.icon(forURL: feedProviderURL, feed: feed) { (image) in - if let image = image { - self.postFeedIconDidBecomeAvailableNotification(feed) - self.cache[feed] = IconImage(image) + Task { @MainActor in + if let image = image { + self.postFeedIconDidBecomeAvailableNotification(feed) + self.cache[feed] = IconImage(image) + } } } return nil @@ -153,7 +159,7 @@ public final class FeedIconDownloader { private extension FeedIconDownloader { - func icon(forHomePageURL homePageURL: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) { + func icon(forHomePageURL homePageURL: String, feed: Feed, _ imageResultBlock: @Sendable @escaping (RSImage?) -> Void) { if homePagesWithNoIconURLCache.contains(homePageURL) || homePagesWithUglyIcons.contains(homePageURL) { imageResultBlock(nil) @@ -168,7 +174,7 @@ private extension FeedIconDownloader { findIconURLForHomePageURL(homePageURL, feed: feed) } - func icon(forURL url: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) { + func icon(forURL url: String, feed: Feed, _ imageResultBlock: @Sendable @escaping (RSImage?) -> Void) { waitingForFeedURLs[url] = feed guard let imageData = imageDownloader.image(for: url) else { imageResultBlock(nil) @@ -255,15 +261,15 @@ private extension FeedIconDownloader { homePagesWithNoIconURLCache = Set(decoded) } - func queueSaveFeedURLToIconURLCacheIfNeeded() { + @MainActor func queueSaveFeedURLToIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveFeedURLToIconURLCacheIfNeeded)) } - func queueSaveHomePageToIconURLCacheIfNeeded() { + @MainActor func queueSaveHomePageToIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveHomePageToIconURLCacheIfNeeded)) } - func queueHomePagesWithNoIconURLCacheIfNeeded() { + @MainActor func queueHomePagesWithNoIconURLCacheIfNeeded() { FeedIconDownloader.saveQueue.add(self, #selector(saveHomePagesWithNoIconURLCacheIfNeeded)) } diff --git a/Shared/ShareExtension/ExtensionContainersFile.swift b/Shared/ShareExtension/ExtensionContainersFile.swift index 7906a77ed..1a3b99214 100644 --- a/Shared/ShareExtension/ExtensionContainersFile.swift +++ b/Shared/ShareExtension/ExtensionContainersFile.swift @@ -22,7 +22,7 @@ final class ExtensionContainersFile { return containerURL!.appendingPathComponent("extension_containers.plist").path }() - private var isDirty = false { + @MainActor private var isDirty = false { didSet { queueSaveToDiskIfNeeded() } @@ -65,15 +65,15 @@ final class ExtensionContainersFile { private extension ExtensionContainersFile { - @objc func markAsDirty() { + @MainActor @objc func markAsDirty() { isDirty = true } - func queueSaveToDiskIfNeeded() { + @MainActor func queueSaveToDiskIfNeeded() { saveQueue.add(self, #selector(saveToDiskIfNeeded)) } - @objc func saveToDiskIfNeeded() { + @MainActor @objc func saveToDiskIfNeeded() { if isDirty { isDirty = false save() diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index c37889c33..e617152af 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -108,7 +108,9 @@ extension SmartFeed: ArticleFetcher { private extension SmartFeed { func queueFetchUnreadCounts() { - CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) + Task { @MainActor in + CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) + } } func fetchUnreadCount(for account: Account) {