diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 84230c147..e1133a3d3 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -670,7 +670,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return try database.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after) } - public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ + @MainActor public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ switch fetchType { case .starred(let limit): return try fetchStarredArticles(limit: limit) @@ -977,29 +977,36 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, NotificationCenter.default.post(name: .AccountRefreshProgressDidChange, object: self) } - @objc func unreadCountDidChange(_ note: Notification) { - if let feed = note.object as? Feed, feed.account === self { - updateUnreadCount() - } + @MainActor @objc func unreadCountDidChange(_ note: Notification) { + guard let feed = note.object as? Feed, feed.account === self else { + return + } +// Task { @MainActor in + updateUnreadCount() +// } } - @objc func batchUpdateDidPerform(_ note: Notification) { - flattenedFeedsNeedUpdate = true - rebuildFeedDictionaries() - updateUnreadCount() + @MainActor @objc func batchUpdateDidPerform(_ note: Notification) { +// Task { @MainActor in + flattenedFeedsNeedUpdate = true + rebuildFeedDictionaries() + updateUnreadCount() +// } } - @objc func childrenDidChange(_ note: Notification) { - guard let object = note.object else { - return - } - if let account = object as? Account, account === self { - structureDidChange() - updateUnreadCount() - } - if let folder = object as? Folder, folder.account === self { - structureDidChange() - } + @MainActor @objc func childrenDidChange(_ note: Notification) { +// Task { @MainActor in + guard let object = note.object else { + return + } + if let account = object as? Account, account === self { + structureDidChange() + updateUnreadCount() + } + if let folder = object as? Folder, folder.account === self { + structureDidChange() + } +// } } @objc func displayNameDidChange(_ note: Notification) { @@ -1054,7 +1061,7 @@ private extension Account { database.fetchedStarredArticlesAsync(flattenedFeeds().feedIDs(), limit, completion) } - func fetchUnreadArticles(limit: Int?) throws -> Set
{ + @MainActor func fetchUnreadArticles(limit: Int?) throws -> Set
{ return try fetchUnreadArticles(forContainer: self, limit: limit) } @@ -1070,7 +1077,7 @@ private extension Account { database.fetchTodayArticlesAsync(flattenedFeeds().feedIDs(), limit, completion) } - func fetchArticles(folder: Folder) throws -> Set
{ + @MainActor func fetchArticles(folder: Folder) throws -> Set
{ return try fetchArticles(forContainer: folder) } @@ -1078,7 +1085,7 @@ private extension Account { fetchArticlesAsync(forContainer: folder, completion) } - func fetchUnreadArticles(folder: Folder) throws -> Set
{ + @MainActor func fetchUnreadArticles(folder: Folder) throws -> Set
{ return try fetchUnreadArticles(forContainer: folder, limit: nil) } @@ -1086,7 +1093,7 @@ private extension Account { fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion) } - func fetchArticles(feed: Feed) throws -> Set
{ + @MainActor func fetchArticles(feed: Feed) throws -> Set
{ let articles = try database.fetchArticles(feed.feedID) validateUnreadCount(feed, articles) return articles @@ -1094,13 +1101,15 @@ private extension Account { func fetchArticlesAsync(feed: Feed, _ completion: @escaping ArticleSetResultBlock) { database.fetchArticlesAsync(feed.feedID) { [weak self] articleSetResult in - switch articleSetResult { - case .success(let articles): - self?.validateUnreadCount(feed, articles) - completion(.success(articles)) - case .failure(let databaseError): - completion(.failure(databaseError)) - } + Task { @MainActor [weak self] in + switch articleSetResult { + case .success(let articles): + self?.validateUnreadCount(feed, articles) + completion(.success(articles)) + case .failure(let databaseError): + completion(.failure(databaseError)) + } + } } } @@ -1128,13 +1137,13 @@ private extension Account { return database.fetchArticlesAsync(articleIDs: articleIDs, completion) } - func fetchUnreadArticles(feed: Feed) throws -> Set
{ + @MainActor func fetchUnreadArticles(feed: Feed) throws -> Set
{ let articles = try database.fetchUnreadArticles(Set([feed.feedID]), nil) validateUnreadCount(feed, articles) return articles } - func fetchArticles(forContainer container: Container) throws -> Set
{ + @MainActor func fetchArticles(forContainer container: Container) throws -> Set
{ let feeds = container.flattenedFeeds() let articles = try database.fetchArticles(feeds.feedIDs()) validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) @@ -1144,17 +1153,19 @@ private extension Account { func fetchArticlesAsync(forContainer container: Container, _ completion: @escaping ArticleSetResultBlock) { let feeds = container.flattenedFeeds() database.fetchArticlesAsync(feeds.feedIDs()) { [weak self] (articleSetResult) in - switch articleSetResult { - case .success(let articles): - self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) - completion(.success(articles)) - case .failure(let databaseError): - completion(.failure(databaseError)) - } + Task { @MainActor [weak self] in + switch articleSetResult { + case .success(let articles): + self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + completion(.success(articles)) + case .failure(let databaseError): + completion(.failure(databaseError)) + } + } } } - func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set
{ + @MainActor func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set
{ let feeds = container.flattenedFeeds() let articles = try database.fetchUnreadArticles(feeds.feedIDs(), limit) @@ -1173,26 +1184,28 @@ private extension Account { return articles } - func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) { - let feeds = container.flattenedFeeds() - database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in - switch articleSetResult { - case .success(let articles): - - // We don't validate limit queries because they, by definition, won't correctly match the - // complete unread state for the given container. - if limit == nil { - self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) - } - - completion(.success(articles)) - case .failure(let databaseError): - completion(.failure(databaseError)) - } - } - } + func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + let feeds = container.flattenedFeeds() + database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in + Task { @MainActor [weak self] in + switch articleSetResult { + case .success(let articles): - func validateUnreadCountsAfterFetchingUnreadArticles(_ feeds: Set, _ articles: Set
) { + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + } + + completion(.success(articles)) + case .failure(let databaseError): + completion(.failure(databaseError)) + } + } + } + } + + @MainActor func validateUnreadCountsAfterFetchingUnreadArticles(_ feeds: Set, _ articles: Set
) { // Validate unread counts. This was the site of a performance slowdown: // it was calling going through the entire list of articles once per feed: // feeds.forEach { validateUnreadCount($0, articles) } @@ -1208,7 +1221,7 @@ private extension Account { } } - func validateUnreadCount(_ feed: Feed, _ articles: Set
) { + @MainActor func validateUnreadCount(_ feed: Feed, _ articles: Set
) { // articles must contain all the unread articles for the feed. // The unread number should match the feed’s unread count. @@ -1265,7 +1278,7 @@ private extension Account { feedDictionariesNeedUpdate = false } - func updateUnreadCount() { + @MainActor func updateUnreadCount() { if fetchingAllUnreadCounts { return } @@ -1317,46 +1330,52 @@ private extension Account { } } - func fetchUnreadCount(_ feed: Feed, _ completion: VoidCompletionBlock?) { - database.fetchUnreadCount(feed.feedID) { result in - if let unreadCount = try? result.get() { - feed.unreadCount = unreadCount - } - completion?() - } - } + func fetchUnreadCount(_ feed: Feed, _ completion: VoidCompletionBlock?) { + database.fetchUnreadCount(feed.feedID) { result in + Task { @MainActor in + if let unreadCount = try? result.get() { + feed.unreadCount = unreadCount + } + completion?() + } + } + } func fetchUnreadCounts(_ feeds: Set, _ completion: VoidCompletionBlock?) { let feedIDs = Set(feeds.map { $0.feedID }) database.fetchUnreadCounts(for: feedIDs) { result in - if let unreadCountDictionary = try? result.get() { - self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: feeds) - } - completion?() + Task { @MainActor in + if let unreadCountDictionary = try? result.get() { + self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: feeds) + } + completion?() + } } } func fetchAllUnreadCounts(_ completion: VoidCompletionBlock? = nil) { fetchingAllUnreadCounts = true database.fetchAllUnreadCounts { result in - guard let unreadCountDictionary = try? result.get() else { - completion?() - return - } - self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: self.flattenedFeeds()) + Task { @MainActor in + guard let unreadCountDictionary = try? result.get() else { + completion?() + return + } + self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: self.flattenedFeeds()) - self.fetchingAllUnreadCounts = false - self.updateUnreadCount() + self.fetchingAllUnreadCounts = false + self.updateUnreadCount() - if !self.isUnreadCountsInitialized { - self.isUnreadCountsInitialized = true - self.postUnreadCountDidInitializeNotification() - } - completion?() + if !self.isUnreadCountsInitialized { + self.isUnreadCountsInitialized = true + self.postUnreadCountDidInitializeNotification() + } + completion?() + } } } - func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set) { + @MainActor func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set) { for feed in feeds { // When the unread count is zero, it won’t appear in unreadCountDictionary. let unreadCount = unreadCountDictionary[feed.feedID] ?? 0 diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 59f652b2e..c0f20db11 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -147,7 +147,7 @@ public final class AccountManager: UnreadCountProvider { return account } - public func deleteAccount(_ account: Account) { + @MainActor public func deleteAccount(_ account: Account) { guard !account.refreshInProgress else { return } @@ -380,7 +380,7 @@ public final class AccountManager: UnreadCountProvider { // These fetch articles from active accounts and return a merged Set
. - public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ + @MainActor public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ precondition(Thread.isMainThread) var articles = Set
() @@ -462,7 +462,7 @@ public final class AccountManager: UnreadCountProvider { // MARK: - Notifications - @objc func unreadCountDidInitialize(_ notification: Notification) { + @MainActor @objc func unreadCountDidInitialize(_ notification: Notification) { guard let _ = notification.object as? Account else { return } @@ -471,14 +471,14 @@ public final class AccountManager: UnreadCountProvider { } } - @objc dynamic func unreadCountDidChange(_ notification: Notification) { + @MainActor @objc func unreadCountDidChange(_ notification: Notification) { guard let _ = notification.object as? Account else { return } updateUnreadCount() } - @objc func accountStateDidChange(_ notification: Notification) { + @MainActor @objc func accountStateDidChange(_ notification: Notification) { updateUnreadCount() } } @@ -487,7 +487,7 @@ public final class AccountManager: UnreadCountProvider { private extension AccountManager { - func updateUnreadCount() { + @MainActor func updateUnreadCount() { unreadCount = calculateUnreadCount(activeAccounts) } diff --git a/Account/Sources/Account/ArticleFetcher.swift b/Account/Sources/Account/ArticleFetcher.swift index e049547fa..91478cb95 100644 --- a/Account/Sources/Account/ArticleFetcher.swift +++ b/Account/Sources/Account/ArticleFetcher.swift @@ -21,7 +21,7 @@ public protocol ArticleFetcher { extension Feed: ArticleFetcher { - public func fetchArticles() throws -> Set
{ + @MainActor public func fetchArticles() throws -> Set
{ return try account?.fetchArticles(.feed(self)) ?? Set
() } @@ -34,7 +34,7 @@ extension Feed: ArticleFetcher { account.fetchArticlesAsync(.feed(self), completion) } - public func fetchUnreadArticles() throws -> Set
{ + @MainActor public func fetchUnreadArticles() throws -> Set
{ return try fetchArticles().unreadArticles() } @@ -61,7 +61,7 @@ extension Feed: ArticleFetcher { extension Folder: ArticleFetcher { - public func fetchArticles() throws -> Set
{ + @MainActor public func fetchArticles() throws -> Set
{ guard let account = account else { assertionFailure("Expected folder.account, but got nil.") return Set
() @@ -78,7 +78,7 @@ extension Folder: ArticleFetcher { account.fetchArticlesAsync(.folder(self, false), completion) } - public func fetchUnreadArticles() throws -> Set
{ + @MainActor public func fetchUnreadArticles() throws -> Set
{ guard let account = account else { assertionFailure("Expected folder.account, but got nil.") return Set
() diff --git a/Account/Sources/Account/Folder.swift b/Account/Sources/Account/Folder.swift index 75bb07a9b..afe52dcc4 100644 --- a/Account/Sources/Account/Folder.swift +++ b/Account/Sources/Account/Folder.swift @@ -87,16 +87,19 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { // MARK: - Notifications - @objc func unreadCountDidChange(_ note: Notification) { - if let object = note.object { - if objectIsChild(object as AnyObject) { - updateUnreadCount() - } - } + @MainActor @objc func unreadCountDidChange(_ note: Notification) { + guard let object = note.object, objectIsChild(object as AnyObject) else { + return + } +// Task { @MainActor in + updateUnreadCount() +// } } - @objc func childrenDidChange(_ note: Notification) { - updateUnreadCount() + @MainActor @objc func childrenDidChange(_ note: Notification) { +// Task { @MainActor in + updateUnreadCount() +// } } // MARK: Container @@ -157,7 +160,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable { private extension Folder { - func updateUnreadCount() { + @MainActor func updateUnreadCount() { var updatedUnreadCount = 0 for feed in topLevelFeeds { updatedUnreadCount += feed.unreadCount diff --git a/Account/Sources/Account/SingleArticleFetcher.swift b/Account/Sources/Account/SingleArticleFetcher.swift index ab3e26e3b..a39c2c7cb 100644 --- a/Account/Sources/Account/SingleArticleFetcher.swift +++ b/Account/Sources/Account/SingleArticleFetcher.swift @@ -20,7 +20,7 @@ public struct SingleArticleFetcher: ArticleFetcher { self.articleID = articleID } - public func fetchArticles() throws -> Set
{ + @MainActor public func fetchArticles() throws -> Set
{ return try account.fetchArticles(.articleIDs(Set([articleID]))) } @@ -28,7 +28,7 @@ public struct SingleArticleFetcher: ArticleFetcher { return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) } - public func fetchUnreadArticles() throws -> Set
{ + @MainActor public func fetchUnreadArticles() throws -> Set
{ return try account.fetchArticles(.articleIDs(Set([articleID]))) } diff --git a/Account/Sources/Account/UnreadCountProvider.swift b/Account/Sources/Account/UnreadCountProvider.swift index f6bc48d4a..fdff3dd61 100644 --- a/Account/Sources/Account/UnreadCountProvider.swift +++ b/Account/Sources/Account/UnreadCountProvider.swift @@ -15,24 +15,24 @@ public extension Notification.Name { public protocol UnreadCountProvider { - var unreadCount: Int { get } + @MainActor var unreadCount: Int { get } - func postUnreadCountDidChangeNotification() - func calculateUnreadCount(_ children: T) -> Int + @MainActor func postUnreadCountDidChangeNotification() + @MainActor func calculateUnreadCount(_ children: T) -> Int } -public extension UnreadCountProvider { +@MainActor public extension UnreadCountProvider { - func postUnreadCountDidInitializeNotification() { + func postUnreadCountDidInitializeNotification() { NotificationCenter.default.post(name: .UnreadCountDidInitialize, object: self, userInfo: nil) } - func postUnreadCountDidChangeNotification() { + func postUnreadCountDidChangeNotification() { NotificationCenter.default.post(name: .UnreadCountDidChange, object: self, userInfo: nil) } - func calculateUnreadCount(_ children: T) -> Int { + func calculateUnreadCount(_ children: T) -> Int { let updatedUnreadCount = children.reduce(0) { (result, oneChild) -> Int in if let oneUnreadCountProvider = oneChild as? UnreadCountProvider { return result + oneUnreadCountProvider.unreadCount diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 7cd64eb4d..21364dbe6 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -9,29 +9,19 @@ import AppKit import UserNotifications import Articles -import RSTree -import RSWeb import Account import RSCore import RSCoreResources import Secrets import CrashReporter -import SwiftUI - -// If we're not going to import Sparkle, provide dummy protocols to make it easy -// for AppDelegate to comply -#if MAC_APP_STORE || TEST -protocol SPUStandardUserDriverDelegate {} -protocol SPUUpdaterDelegate {} -#else +#if !MAC_APP_STORE && !TEST import Sparkle #endif var appDelegate: AppDelegate! @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate, Logging -{ +@MainActor class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, Logging { private struct WindowRestorationIdentifiers { static let mainWindow = "mainWindow" @@ -110,7 +100,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var crashReporter: PLCrashReporter! #endif - @MainActor override init() { + override init() { NSWindow.allowsAutomaticWindowTabbing = false super.init() @@ -140,19 +130,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - API - @MainActor func showAddFolderSheetOnWindow(_ window: NSWindow) { + func showAddFolderSheetOnWindow(_ window: NSWindow) { addFolderWindowController = AddFolderWindowController() addFolderWindowController!.runSheetOnWindow(window) } - @MainActor func showAddFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) { + func showAddFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) { addFeedController = AddFeedController(hostWindow: window) addFeedController?.showAddFeedSheet(.feed, urlString, name, account, folder) } // MARK: - NSApplicationDelegate - @MainActor func applicationWillFinishLaunching(_ notification: Notification) { + func applicationWillFinishLaunching(_ notification: Notification) { installAppleEventHandlers() CacheCleaner.purgeIfNecessary() @@ -183,7 +173,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String) } - @MainActor func applicationDidFinishLaunching(_ note: Notification) { + func applicationDidFinishLaunching(_ note: Notification) { #if MAC_APP_STORE || TEST checkForUpdatesMenuItem.isHidden = true @@ -296,7 +286,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } - @MainActor func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { + func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { guard let mainWindowController = mainWindowController else { return false } @@ -304,7 +294,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return true } - @MainActor func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { // https://github.com/brentsimmons/NetNewsWire/issues/522 // I couldn’t reproduce the crashing bug, but it appears to happen on creating a main window // and its views and view controllers. The check below is so that the app does nothing @@ -318,26 +308,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return false } - @MainActor func applicationDidBecomeActive(_ notification: Notification) { + func applicationDidBecomeActive(_ notification: Notification) { fireOldTimers() } - @MainActor func applicationDidResignActive(_ notification: Notification) { + func applicationDidResignActive(_ notification: Notification) { ArticleStringFormatter.emptyCaches() saveState() } - @MainActor func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { + func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) } - @MainActor func application(_ sender: NSApplication, openFile filename: String) -> Bool { + func application(_ sender: NSApplication, openFile filename: String) -> Bool { guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } importTheme(filename: filename) return true } - @MainActor func applicationWillTerminate(_ notification: Notification) { + func applicationWillTerminate(_ notification: Notification) { shuttingDown = true saveState() @@ -351,7 +341,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { } } - @MainActor func presentThemeImportError(_ error: Error) { + func presentThemeImportError(_ error: Error) { var informativeText: String = "" if let decodingError = error as? DecodingError { @@ -396,13 +386,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: Notifications - @MainActor @objc func unreadCountDidChange(_ note: Notification) { + @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { unreadCount = AccountManager.shared.unreadCount } } - @MainActor @objc func feedSettingDidChange(_ note: Notification) { + @objc func feedSettingDidChange(_ note: Notification) { guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else { return } @@ -411,14 +401,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @MainActor @objc func inspectableObjectsDidChange(_ note: Notification) { + @objc func inspectableObjectsDidChange(_ note: Notification) { guard let inspectorWindowController = inspectorWindowController, inspectorWindowController.isOpen else { return } inspectorWindowController.objects = objectsForInspector() } - @MainActor @objc func userDefaultsDidChange(_ note: Notification) { + @objc func userDefaultsDidChange(_ note: Notification) { updateSortMenuItems() updateGroupByFeedMenuItem() @@ -430,11 +420,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, updateDockBadge() } - @MainActor @objc func didWakeNotification(_ note: Notification) { + @objc func didWakeNotification(_ note: Notification) { fireOldTimers() } - @MainActor @objc func importDownloadedTheme(_ note: Notification) { + @objc func importDownloadedTheme(_ note: Notification) { guard let userInfo = note.userInfo, let url = userInfo["url"] as? URL else { return @@ -446,7 +436,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: Main Window - @MainActor func createMainWindowController() -> MainWindowController { + func createMainWindowController() -> MainWindowController { let controller: MainWindowController controller = windowControllerWithName("MainWindow") as! MainWindowController @@ -457,13 +447,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return controller } - @MainActor func windowControllerWithName(_ storyboardName: String) -> NSWindowController { + func windowControllerWithName(_ storyboardName: String) -> NSWindowController { let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil) return storyboard.instantiateInitialController()! as! NSWindowController } @discardableResult - @MainActor func createAndShowMainWindow() -> MainWindowController { + func createAndShowMainWindow() -> MainWindowController { let controller = createMainWindowController() controller.showWindow(self) @@ -475,7 +465,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return controller } - @MainActor func createAndShowMainWindowIfNecessary() { + func createAndShowMainWindowIfNecessary() { if mainWindowController == nil { createAndShowMainWindow() } else { @@ -483,7 +473,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @MainActor func removeMainWindow(_ windowController: MainWindowController) { + func removeMainWindow(_ windowController: MainWindowController) { guard mainWindowControllers.count > 1 else { return } if let index = mainWindowControllers.firstIndex(of: windowController) { mainWindowControllers.remove(at: index) @@ -491,7 +481,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: NSUserInterfaceValidations - @MainActor func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { + func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { if shuttingDown { return false } @@ -529,27 +519,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: UNUserNotificationCenterDelegate - @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.banner, .badge, .sound]) - } + nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + [.banner, .badge, .sound] + } - @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - let userInfo = response.notification.request.content.userInfo + await MainActor.run { + let userInfo = response.notification.request.content.userInfo - switch response.actionIdentifier { - case "MARK_AS_READ": - handleMarkAsRead(userInfo: userInfo) - case "MARK_AS_STARRED": - handleMarkAsStarred(userInfo: userInfo) - default: - mainWindowController?.handle(response) + switch response.actionIdentifier { + case "MARK_AS_READ": + handleMarkAsRead(userInfo: userInfo) + case "MARK_AS_STARRED": + handleMarkAsStarred(userInfo: userInfo) + default: + mainWindowController?.handle(response) + } } - completionHandler() - } + } // MARK: Add Feed - @MainActor func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { + func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { @@ -560,13 +551,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - Dock Badge - @MainActor @objc func updateDockBadge() { + @objc func updateDockBadge() { let label = unreadCount > 0 ? "\(unreadCount)" : "" NSApplication.shared.dockTile.badgeLabel = label } // MARK: - Actions - @MainActor @IBAction func showPreferences(_ sender: Any?) { + @IBAction func showPreferences(_ sender: Any?) { if preferencesWindowController == nil { preferencesWindowController = windowControllerWithName("Preferences") } @@ -574,29 +565,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, preferencesWindowController!.showWindow(self) } - @MainActor @IBAction func newMainWindow(_ sender: Any?) { + @IBAction func newMainWindow(_ sender: Any?) { createAndShowMainWindow() } - @MainActor @IBAction func showMainWindow(_ sender: Any?) { + @IBAction func showMainWindow(_ sender: Any?) { createAndShowMainWindowIfNecessary() mainWindowController?.window?.makeKey() } - @MainActor @IBAction func refreshAll(_ sender: Any?) { + @IBAction func refreshAll(_ sender: Any?) { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) } - @MainActor @IBAction func showAddFeedWindow(_ sender: Any?) { + @IBAction func showAddFeedWindow(_ sender: Any?) { addFeed(nil) } - @MainActor @IBAction func showAddFolderWindow(_ sender: Any?) { + @IBAction func showAddFolderWindow(_ sender: Any?) { createAndShowMainWindowIfNecessary() showAddFolderSheetOnWindow(mainWindowController!.window!) } - @MainActor @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { + @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { if keyboardShortcutsWindowController == nil { keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("window.title.keyboard-shortcuts", comment: "Keyboard Shortcuts")) @@ -615,7 +606,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, keyboardShortcutsWindowController!.showWindow(self) } - @MainActor @IBAction func toggleInspectorWindow(_ sender: Any?) { + @IBAction func toggleInspectorWindow(_ sender: Any?) { if inspectorWindowController == nil { inspectorWindowController = (windowControllerWithName("Inspector") as! InspectorWindowController) } @@ -629,7 +620,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @MainActor @IBAction func importOPMLFromFile(_ sender: Any?) { + @IBAction func importOPMLFromFile(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return @@ -639,7 +630,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, importOPMLController?.runSheetOnWindow(mainWindowController!.window!) } - @MainActor @IBAction func importNNW3FromFile(_ sender: Any?) { + @IBAction func importNNW3FromFile(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return @@ -647,7 +638,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, NNW3ImportController.askUserToImportNNW3Subscriptions(window: mainWindowController!.window!) } - @MainActor @IBAction func exportOPML(_ sender: Any?) { + @IBAction func exportOPML(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return @@ -657,30 +648,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, exportOPMLController?.runSheetOnWindow(mainWindowController!.window!) } - @MainActor @IBAction func addAppNews(_ sender: Any?) { + @IBAction func addAppNews(_ sender: Any?) { if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() { return } addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News") } - @MainActor @IBAction func openWebsite(_ sender: Any?) { + @IBAction func openWebsite(_ sender: Any?) { Browser.open("https://netnewswire.com/", inBackground: false) } - @MainActor @IBAction func showHelp(_ sender: Any?) { + @IBAction func showHelp(_ sender: Any?) { Browser.open("https://netnewswire.com/help/mac/6.1/en/", inBackground: false) } - @MainActor @IBAction func gotoToday(_ sender: Any?) { + @IBAction func gotoToday(_ sender: Any?) { createAndShowMainWindowIfNecessary() mainWindowController!.gotoToday(sender) } - @MainActor @IBAction func gotoAllUnread(_ sender: Any?) { + @IBAction func gotoAllUnread(_ sender: Any?) { createAndShowMainWindowIfNecessary() mainWindowController!.gotoAllUnread(sender) @@ -692,27 +683,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, mainWindowController!.gotoStarred(sender) } - @MainActor @IBAction func sortByOldestArticleOnTop(_ sender: Any?) { + @IBAction func sortByOldestArticleOnTop(_ sender: Any?) { AppDefaults.shared.timelineSortDirection = .orderedAscending } - @MainActor @IBAction func sortByNewestArticleOnTop(_ sender: Any?) { + @IBAction func sortByNewestArticleOnTop(_ sender: Any?) { AppDefaults.shared.timelineSortDirection = .orderedDescending } - @MainActor @IBAction func groupByFeedToggled(_ sender: NSMenuItem) { + @IBAction func groupByFeedToggled(_ sender: NSMenuItem) { AppDefaults.shared.timelineGroupByFeed.toggle() } - @MainActor @IBAction func checkForUpdates(_ sender: Any?) { + @IBAction func checkForUpdates(_ sender: Any?) { #if !MAC_APP_STORE && !TEST self.softwareUpdater.checkForUpdates() #endif } - @MainActor @IBAction func showAbout(_ sender: Any?) { + @IBAction func showAbout(_ sender: Any?) { if #available(macOS 12, *) { for window in NSApplication.shared.windows { if window.identifier == .aboutNetNewsWire { @@ -731,7 +722,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: - NSMenuDelegate -@MainActor extension AppDelegate: NSMenuDelegate { +extension AppDelegate: NSMenuDelegate { public func menuNeedsUpdate(_ menu: NSMenu) { let newShareMenu = mainWindowController?.shareMenu @@ -750,7 +741,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: - Debug Menu -@MainActor extension AppDelegate { +extension AppDelegate { @IBAction func debugSearch(_ sender: Any?) { AccountManager.shared.defaultAccount.debugRunSearch() @@ -822,7 +813,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } -@MainActor internal extension AppDelegate { +// MARK: - Sparkle + +extension AppDelegate: SPUStandardUserDriverDelegate, SPUUpdaterDelegate { +} + +extension AppDelegate { func fireOldTimers() { // It’s possible there’s a refresh timer set to go off in the past. @@ -1023,7 +1019,7 @@ extension AppDelegate : ScriptingAppDelegate { } } -@MainActor extension AppDelegate: NSWindowRestoration { +extension AppDelegate: NSWindowRestoration { @objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) { var mainWindow: NSWindow? = nil @@ -1037,7 +1033,7 @@ extension AppDelegate : ScriptingAppDelegate { // Handle Notification Actions -@MainActor private extension AppDelegate { +private extension AppDelegate { func handleMarkAsRead(userInfo: [AnyHashable: Any]) { markArticle(userInfo: userInfo, statusKey: .read) diff --git a/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift index 334605cbe..549c9e5c3 100644 --- a/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift +++ b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift @@ -14,7 +14,7 @@ final class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler { var currentArticle: Article? - func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + @MainActor func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else { urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) diff --git a/Mac/Scriptability/WebFeed+Scriptability.swift b/Mac/Scriptability/WebFeed+Scriptability.swift index 2377c9135..3ed8d72ac 100644 --- a/Mac/Scriptability/WebFeed+Scriptability.swift +++ b/Mac/Scriptability/WebFeed+Scriptability.swift @@ -164,7 +164,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine } @objc(articles) - var articles:NSArray { + @MainActor var articles:NSArray { let feedArticles = (try? feed.fetchArticles()) ?? Set
() // the articles are a set, use the sorting algorithm from the viewer let sortedArticles = feedArticles.sorted(by:{ @@ -174,7 +174,7 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine } @objc(valueInArticlesWithUniqueID:) - func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { + @MainActor func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { let articles = (try? feed.fetchArticles()) ?? Set
() guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil } return ScriptableArticle(article, container:self) diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index 2229ffbd9..f5c5fd6ba 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -49,7 +49,7 @@ class ActivityManager { invalidateNextUnread() } - func selecting(feed: FeedProtocol) { + @MainActor func selecting(feed: FeedProtocol) { invalidateCurrentActivities() selectingActivity = makeSelectFeedActivity(feed: feed) @@ -87,7 +87,7 @@ class ActivityManager { nextUnreadActivity = nil } - func reading(feed: FeedProtocol?, article: Article?) { + @MainActor func reading(feed: FeedProtocol?, article: Article?) { invalidateReading() invalidateNextUnread() @@ -108,7 +108,7 @@ class ActivityManager { } #if os(iOS) - static func cleanUp(_ account: Account) { + @MainActor static func cleanUp(_ account: Account) { var ids = [String]() if let folders = account.folders { @@ -124,7 +124,7 @@ class ActivityManager { CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } - static func cleanUp(_ folder: Folder) { + @MainActor static func cleanUp(_ folder: Folder) { var ids = [String]() ids.append(identifier(for: folder)) @@ -135,12 +135,12 @@ class ActivityManager { CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } - static func cleanUp(_ feed: Feed) { + @MainActor static func cleanUp(_ feed: Feed) { CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: feed)) } #endif - @objc func feedIconDidBecomeAvailable(_ note: Notification) { + @MainActor @objc func feedIconDidBecomeAvailable(_ note: Notification) { guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedID = selectingActivity?.userInfo?[ArticlePathKey.feedID] as? String else { return } @@ -187,7 +187,7 @@ private extension ActivityManager { return activity } - func makeReadArticleActivity(feed: FeedProtocol?, article: Article) -> NSUserActivity { + @MainActor func makeReadArticleActivity(feed: FeedProtocol?, article: Article) -> NSUserActivity { let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue) activity.title = ArticleStringFormatter.truncatedTitle(article) @@ -217,7 +217,7 @@ private extension ActivityManager { } #if os(iOS) - func updateReadArticleSearchAttributes(with article: Article) { + @MainActor func updateReadArticleSearchAttributes(with article: Article) { let attributeSet = CSSearchableItemAttributeSet(itemContentType: UTType.compositeContent.identifier) attributeSet.title = ArticleStringFormatter.truncatedTitle(article) @@ -245,7 +245,7 @@ private extension ActivityManager { return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? [] } - func updateSelectingActivityFeedSearchAttributes(with feed: Feed) { + @MainActor func updateSelectingActivityFeedSearchAttributes(with feed: Feed) { let attributeSet = CSSearchableItemAttributeSet(itemContentType: UTType.item.identifier) attributeSet.title = feed.nameForDisplay @@ -286,7 +286,7 @@ private extension ActivityManager { return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)" } - static func identifiers(for feed: Feed) -> [String] { + @MainActor static func identifiers(for feed: Feed) -> [String] { var ids = [String]() ids.append(identifier(for: feed)) if let articles = try? feed.fetchArticles() { diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index 65034a38d..8a1723a3b 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -112,11 +112,11 @@ extension Article { } } - func iconImage() -> IconImage? { + @MainActor func iconImage() -> IconImage? { return IconImageCache.shared.imageForArticle(self) } - func iconImageUrl(feed: Feed) -> URL? { + @MainActor func iconImageUrl(feed: Feed) -> URL? { if let image = iconImage() { let fm = FileManager.default var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] diff --git a/Shared/Extensions/SmallIconProvider.swift b/Shared/Extensions/SmallIconProvider.swift index 195650024..52f36b342 100644 --- a/Shared/Extensions/SmallIconProvider.swift +++ b/Shared/Extensions/SmallIconProvider.swift @@ -27,7 +27,7 @@ extension Account: SmallIconProvider { extension Feed: SmallIconProvider { - var smallIcon: IconImage? { + @MainActor var smallIcon: IconImage? { if let iconImage = appDelegate.faviconDownloader.favicon(for: self) { return iconImage } diff --git a/Shared/IconImageCache.swift b/Shared/IconImageCache.swift index 16831e7fe..c8b48265a 100644 --- a/Shared/IconImageCache.swift +++ b/Shared/IconImageCache.swift @@ -20,7 +20,7 @@ class IconImageCache { private var smallIconImageCache = [ItemIdentifier: IconImage]() private var authorIconImageCache = [Author: IconImage]() - func imageFor(_ itemID: ItemIdentifier) -> IconImage? { + @MainActor func imageFor(_ itemID: ItemIdentifier) -> IconImage? { if let smartFeed = SmartFeedsController.shared.find(by: itemID) { return imageForFeed(smartFeed) } @@ -30,7 +30,7 @@ class IconImageCache { return nil } - func imageForFeed(_ feedProtocol: FeedProtocol) -> IconImage? { + @MainActor func imageForFeed(_ feedProtocol: FeedProtocol) -> IconImage? { guard let itemID = feedProtocol.itemID else { return nil } @@ -48,7 +48,7 @@ class IconImageCache { return nil } - func imageForArticle(_ article: Article) -> IconImage? { + @MainActor func imageForArticle(_ article: Article) -> IconImage? { if let iconImage = imageForAuthors(article.authors) { return iconImage } @@ -80,7 +80,7 @@ private extension IconImageCache { return nil } - func imageForFeed(_ feed: Feed, _ itemID: ItemIdentifier) -> IconImage? { + @MainActor func imageForFeed(_ feed: Feed, _ itemID: ItemIdentifier) -> IconImage? { if let iconImage = feedIconImageCache[itemID] { return iconImage } @@ -109,14 +109,14 @@ private extension IconImageCache { return nil } - func imageForAuthors(_ authors: Set?) -> IconImage? { + @MainActor func imageForAuthors(_ authors: Set?) -> IconImage? { guard let authors = authors, authors.count == 1, let author = authors.first else { return nil } return imageForAuthor(author) } - func imageForAuthor(_ author: Author) -> IconImage? { + @MainActor func imageForAuthor(_ author: Author) -> IconImage? { if let iconImage = authorIconImageCache[author] { return iconImage } diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index 4caafaced..c3d6a3686 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -61,7 +61,7 @@ final class SmartFeed: PseudoFeed { } } - @objc func fetchUnreadCounts() { + @MainActor @objc func fetchUnreadCounts() { let activeAccounts = AccountManager.shared.activeAccounts // Remove any accounts that are no longer active or have been deleted @@ -110,7 +110,7 @@ private extension SmartFeed { CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) } - func fetchUnreadCount(for account: Account) { + @MainActor func fetchUnreadCount(for account: Account) { delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in guard let accountUnreadCount = try? singleUnreadCountResult.get() else { return @@ -120,7 +120,7 @@ private extension SmartFeed { } } - func updateUnreadCount() { + @MainActor func updateUnreadCount() { unreadCount = AccountManager.shared.activeAccounts.reduce(0) { (result, account) -> Int in if let oneUnreadCount = unreadCounts[account.accountID] { return result + oneUnreadCount diff --git a/Shared/SmartFeeds/SmartFeedDelegate.swift b/Shared/SmartFeeds/SmartFeedDelegate.swift index 39c85597a..d30b53f13 100644 --- a/Shared/SmartFeeds/SmartFeedDelegate.swift +++ b/Shared/SmartFeeds/SmartFeedDelegate.swift @@ -19,7 +19,7 @@ protocol SmartFeedDelegate: ItemIdentifiable, DisplayNameProvider, ArticleFetche extension SmartFeedDelegate { - func fetchArticles() throws -> Set
{ + @MainActor func fetchArticles() throws -> Set
{ return try AccountManager.shared.fetchArticles(fetchType) } diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index 273c760d9..afe330aab 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -21,7 +21,7 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { var smartFeeds = [FeedProtocol]() let todayFeed = SmartFeed(delegate: TodayFeedDelegate()) - let unreadFeed = UnreadFeed() + @MainActor let unreadFeed = UnreadFeed() let starredFeed = SmartFeed(delegate: StarredFeedDelegate()) private init() { diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 1efedff06..79d15bd24 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -51,13 +51,13 @@ final class UnreadFeed: PseudoFeed { } #endif - init() { + @MainActor init() { self.unreadCount = appDelegate.unreadCount NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: appDelegate) } - @objc func unreadCountDidChange(_ note: Notification) { + @MainActor @objc func unreadCountDidChange(_ note: Notification) { assert(note.object is AppDelegate) unreadCount = appDelegate.unreadCount @@ -66,7 +66,7 @@ final class UnreadFeed: PseudoFeed { extension UnreadFeed: ArticleFetcher { - func fetchArticles() throws -> Set
{ + @MainActor func fetchArticles() throws -> Set
{ return try fetchUnreadArticles() } @@ -74,7 +74,7 @@ extension UnreadFeed: ArticleFetcher { fetchUnreadArticlesAsync(completion) } - func fetchUnreadArticles() throws -> Set
{ + @MainActor func fetchUnreadArticles() throws -> Set
{ return try AccountManager.shared.fetchArticles(fetchType) } diff --git a/Shared/Tree/FeedTreeControllerDelegate.swift b/Shared/Tree/FeedTreeControllerDelegate.swift index edc789d02..846d01077 100644 --- a/Shared/Tree/FeedTreeControllerDelegate.swift +++ b/Shared/Tree/FeedTreeControllerDelegate.swift @@ -24,7 +24,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate { filterExceptions = Set() } - func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { + @MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { if node.isRoot { return childNodesForRootNode(node) } @@ -61,7 +61,7 @@ private extension FeedTreeControllerDelegate { } } - func childNodesForContainerNode(_ containerNode: Node) -> [Node]? { + @MainActor func childNodesForContainerNode(_ containerNode: Node) -> [Node]? { let container = containerNode.representedObject as! Container var children = [AnyObject]() diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index b04c5d262..f35e8eeaa 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -20,7 +20,7 @@ final class UserNotificationManager: NSObject { registerCategoriesAndActions() } - @objc func accountDidDownloadArticles(_ note: Notification) { + @MainActor @objc func accountDidDownloadArticles(_ note: Notification) { guard let articles = note.userInfo?[Account.UserInfoKey.newArticles] as? Set
else { return } @@ -53,7 +53,7 @@ final class UserNotificationManager: NSObject { private extension UserNotificationManager { - func sendNotification(feed: Feed, article: Article) { + @MainActor func sendNotification(feed: Feed, article: Article) { let content = UNMutableNotificationContent() content.title = feed.nameForDisplay @@ -73,13 +73,13 @@ private extension UserNotificationManager { UNUserNotificationCenter.current().add(request) } - /// Determine if there is an available icon for the article. This will then move it to the caches directory and make it avialble for the notification. + /// Determine if there is an available icon for the article. This will then move it to the caches directory and make it available for the notification. /// - Parameters: /// - article: `Article` /// - feed: `Feed` /// - Returns: A `UNNotificationAttachment` if an icon is available. Otherwise nil. /// - Warning: In certain scenarios, this will return the `faviconTemplateImage`. - func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? { + @MainActor func thumbnailAttachment(for article: Article, feed: Feed) -> UNNotificationAttachment? { if let imageURL = article.iconImageUrl(feed: feed) { let thumbnail = try? UNNotificationAttachment(identifier: feed.feedID, url: imageURL, options: nil) return thumbnail diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index acd09f91c..0d6fbbf8f 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -67,7 +67,7 @@ public final class WidgetDataEncoder { } } - private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) { + @MainActor private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) { let dispatchGroup = DispatchGroup() var groupError: Error? = nil diff --git a/iOS/Article/ArticleIconSchemeHandler.swift b/iOS/Article/ArticleIconSchemeHandler.swift index 62fe0ee62..1c29470ce 100644 --- a/iOS/Article/ArticleIconSchemeHandler.swift +++ b/iOS/Article/ArticleIconSchemeHandler.swift @@ -18,7 +18,7 @@ class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler { self.coordinator = coordinator } - func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + @MainActor func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { guard let url = urlSchemeTask.request.url, let coordinator = coordinator else { urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) diff --git a/iOS/Inspector/AccountInspectorView.swift b/iOS/Inspector/AccountInspectorView.swift index c67450946..41d1c4214 100644 --- a/iOS/Inspector/AccountInspectorView.swift +++ b/iOS/Inspector/AccountInspectorView.swift @@ -98,7 +98,7 @@ struct AccountInspectorView: View { } } - var removeAccountSection: some View { + @MainActor var removeAccountSection: some View { Section { Button(role: .destructive) { showRemoveAccountAlert = true diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift index 885a148cb..77d51bdec 100644 --- a/iOS/Inspector/FeedInspectorView.swift +++ b/iOS/Inspector/FeedInspectorView.swift @@ -59,7 +59,7 @@ struct FeedInspectorView: View { .dismissOnExternalContextLaunch() } - var feedHeaderView: some View { + @MainActor var feedHeaderView: some View { HStack { Spacer() Image(uiImage: feed.smallIcon!.image) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 930289c11..d7fc80bbf 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -159,7 +159,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { // At some point we should refactor the current Feed IndexPath out and only use the timeline feed private(set) var currentFeedIndexPath: IndexPath? - var timelineIconImage: IconImage? { + @MainActor var timelineIconImage: IconImage? { guard let timelineFeed = timelineFeed else { return nil } @@ -280,7 +280,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { return timelineUnreadCount > 0 } - var isAnyUnreadAvailable: Bool { + @MainActor var isAnyUnreadAvailable: Bool { return appDelegate.unreadCount > 0 } @@ -372,7 +372,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func handle(_ activity: NSUserActivity) { + @MainActor func handle(_ activity: NSUserActivity) { selectFeed(indexPath: nil) { guard let activityType = ActivityType(rawValue: activity.activityType) else { return } switch activityType { @@ -390,7 +390,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func handle(_ response: UNNotificationResponse) { + @MainActor func handle(_ response: UNNotificationResponse) { let userInfo = response.notification.request.content.userInfo handleReadArticle(userInfo) } @@ -403,7 +403,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func selectFirstUnreadInAllUnread() { + @MainActor func selectFirstUnreadInAllUnread() { markExpanded(SmartFeedsController.shared) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) { self.selectFeed(SmartFeedsController.shared.unreadFeed) { @@ -412,7 +412,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func showSearch() { + @MainActor func showSearch() { selectFeed(indexPath: nil) { self.rootSplitViewController.show(.supplementary) DispatchQueue.main.asyncAfter(deadline: .now()) { @@ -679,7 +679,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { return indexPath } - func unreadCountFor(_ node: Node) -> Int { + @MainActor func unreadCountFor(_ node: Node) -> Int { // The coordinator supplies the unread count for the currently selected feed if node.representedObject === timelineFeed as AnyObject { return timelineUnreadCount @@ -743,7 +743,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { rebuildBackingStores() } - func collapse(_ containerID: ContainerIdentifier) { + @MainActor func collapse(_ containerID: ContainerIdentifier) { unmarkExpanded(containerID) rebuildBackingStores() clearTimelineIfNoLongerAvailable() @@ -751,13 +751,13 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { /// This is a special function that expects the caller to change the disclosure arrow state outside this function. /// Failure to do so will get the Sidebar into an invalid state. - func collapse(_ node: Node) { + @MainActor func collapse(_ node: Node) { guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return } lastExpandedTable.remove(containerID) collapse(containerID) } - func collapseAllFolders() { + @MainActor func collapseAllFolders() { for sectionNode in treeController.rootNode.childNodes { for topLevelNode in sectionNode.childNodes { if topLevelNode.representedObject is Folder { @@ -776,7 +776,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { return indexPathFor(node) } - func selectFeed(_ feed: FeedProtocol?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { + @MainActor func selectFeed(_ feed: FeedProtocol?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { let indexPath: IndexPath? = { if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) { return indexPath @@ -787,7 +787,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { selectFeed(indexPath: indexPath, animations: animations, deselectArticle: deselectArticle, completion: completion) } - func selectFeed(indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { + @MainActor func selectFeed(indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { guard indexPath != currentFeedIndexPath else { completion?() return @@ -826,40 +826,40 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } - func selectPrevFeed() { + @MainActor func selectPrevFeed() { if let indexPath = prevFeedIndexPath { selectFeed(indexPath: indexPath, animations: [.navigation, .scroll]) } } - func selectNextFeed() { + @MainActor func selectNextFeed() { if let indexPath = nextFeedIndexPath { selectFeed(indexPath: indexPath, animations: [.navigation, .scroll]) } } - func selectTodayFeed(completion: (() -> Void)? = nil) { + @MainActor func selectTodayFeed(completion: (() -> Void)? = nil) { markExpanded(SmartFeedsController.shared) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.todayFeed) { self.selectFeed(SmartFeedsController.shared.todayFeed, animations: [.navigation, .scroll], completion: completion) } } - func selectAllUnreadFeed(completion: (() -> Void)? = nil) { + @MainActor func selectAllUnreadFeed(completion: (() -> Void)? = nil) { markExpanded(SmartFeedsController.shared) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) { self.selectFeed(SmartFeedsController.shared.unreadFeed, animations: [.navigation, .scroll], completion: completion) } } - func selectStarredFeed(completion: (() -> Void)? = nil) { + @MainActor func selectStarredFeed(completion: (() -> Void)? = nil) { markExpanded(SmartFeedsController.shared) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.starredFeed) { self.selectFeed(SmartFeedsController.shared.starredFeed, animations: [.navigation, .scroll], completion: completion) } } - func selectArticle(_ article: Article?, animations: Animations = [], isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { + @MainActor func selectArticle(_ article: Article?, animations: Animations = [], isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { guard article != currentArticle else { return } currentArticle = article @@ -883,7 +883,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func beginSearching() { + @MainActor func beginSearching() { isSearching = true preSearchTimelineFeed = timelineFeed savedSearchArticles = articles @@ -892,7 +892,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { selectArticle(nil) } - func endSearching() { + @MainActor func endSearching() { if let oldTimelineFeed = preSearchTimelineFeed { emptyTheTimeline() timelineFeed = oldTimelineFeed @@ -950,25 +950,25 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { return articles[index + 1] } - func selectPrevArticle() { + @MainActor func selectPrevArticle() { if let article = prevArticle { selectArticle(article, animations: [.navigation, .scroll]) } } - func selectNextArticle() { + @MainActor func selectNextArticle() { if let article = nextArticle { selectArticle(article, animations: [.navigation, .scroll]) } } - func selectFirstUnread() { + @MainActor func selectFirstUnread() { if selectFirstUnreadArticleInTimeline() { activityManager.selectingNextUnread() } } - func selectPrevUnread() { + @MainActor func selectPrevUnread() { // This should never happen, but I don't want to risk throwing us // into an infinite loop searching for an unread that isn't there. @@ -989,7 +989,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { selectPrevUnreadArticleInTimeline() } - func selectNextUnread() { + @MainActor func selectNextUnread() { // This should never happen, but I don't want to risk throwing us // into an infinite loop searching for an unread that isn't there. @@ -1015,7 +1015,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func scrollOrGoToNextUnread() { + @MainActor func scrollOrGoToNextUnread() { if articleViewController?.canScrollDown() ?? false { articleViewController?.scrollPageDown() } else { @@ -1153,7 +1153,9 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { rebuildBackingStores(initialLoad: initialLoad, completion: { self.treeControllerDelegate.resetFilterExceptions() - self.selectFeed(feed, animations: animations, completion: completion) + Task { @MainActor in + self.selectFeed(feed, animations: animations, completion: completion) + } }) } @@ -1197,7 +1199,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { rootSplitViewController.present(hosting, animated: true) } - func showAddFeed(initialFeed: String? = nil, initialFeedName: String? = nil) { + @MainActor func showAddFeed(initialFeed: String? = nil, initialFeedName: String? = nil) { // Since Add Feed can be opened from anywhere with a keyboard shortcut, we have to deselect any currently selected feeds selectFeed(nil) @@ -1270,12 +1272,12 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - func navigateToFeeds() { + @MainActor func navigateToFeeds() { masterFeedViewController?.focus() selectArticle(nil) } - func navigateToTimeline() { + @MainActor func navigateToTimeline() { if currentArticle == nil && articles.count > 0 { selectArticle(articles[0]) } @@ -1290,7 +1292,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { rootSplitViewController.preferredDisplayMode = rootSplitViewController.displayMode == .oneBesideSecondary ? .secondaryOnly : .oneBesideSecondary } - func selectArticleInCurrentFeed(_ articleID: String, isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { + @MainActor func selectArticleInCurrentFeed(_ articleID: String, isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { if let article = self.articles.first(where: { $0.articleID == articleID }) { self.selectArticle(article, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } @@ -1657,7 +1659,7 @@ private extension SceneCoordinator { return false } - func clearTimelineIfNoLongerAvailable() { + @MainActor func clearTimelineIfNoLongerAvailable() { if let feed = timelineFeed, !shadowTableContains(feed) { selectFeed(nil, deselectArticle: true) } @@ -1746,7 +1748,7 @@ private extension SceneCoordinator { // MARK: Select Prev Unread @discardableResult - func selectPrevUnreadArticleInTimeline() -> Bool { + @MainActor func selectPrevUnreadArticleInTimeline() -> Bool { let startingRow: Int = { if let articleRow = currentArticleRow { return articleRow @@ -1758,7 +1760,7 @@ private extension SceneCoordinator { return selectPrevArticleInTimeline(startingRow: startingRow) } - func selectPrevArticleInTimeline(startingRow: Int) -> Bool { + @MainActor func selectPrevArticleInTimeline(startingRow: Int) -> Bool { guard startingRow >= 0 else { return false @@ -1776,7 +1778,7 @@ private extension SceneCoordinator { } - func selectPrevUnreadFeedFetcher() { + @MainActor func selectPrevUnreadFeedFetcher() { let indexPath: IndexPath = { if currentFeedIndexPath == nil { @@ -1808,7 +1810,7 @@ private extension SceneCoordinator { } @discardableResult - func selectPrevUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool { + @MainActor func selectPrevUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool { for i in (0...indexPath.section).reversed() { @@ -1848,12 +1850,12 @@ private extension SceneCoordinator { // MARK: Select Next Unread @discardableResult - func selectFirstUnreadArticleInTimeline() -> Bool { + @MainActor func selectFirstUnreadArticleInTimeline() -> Bool { return selectNextArticleInTimeline(startingRow: 0, animated: true) } @discardableResult - func selectNextUnreadArticleInTimeline() -> Bool { + @MainActor func selectNextUnreadArticleInTimeline() -> Bool { let startingRow: Int = { if let articleRow = currentArticleRow { return articleRow + 1 @@ -1865,7 +1867,7 @@ private extension SceneCoordinator { return selectNextArticleInTimeline(startingRow: startingRow, animated: false) } - func selectNextArticleInTimeline(startingRow: Int, animated: Bool) -> Bool { + @MainActor func selectNextArticleInTimeline(startingRow: Int, animated: Bool) -> Bool { guard startingRow < articles.count else { return false @@ -1883,7 +1885,7 @@ private extension SceneCoordinator { } - func selectNextUnreadFeed(completion: @escaping () -> Void) { + @MainActor func selectNextUnreadFeed(completion: @escaping () -> Void) { let indexPath: IndexPath = { if currentFeedIndexPath == nil { @@ -1918,7 +1920,7 @@ private extension SceneCoordinator { } - func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) { + @MainActor func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) { for i in indexPath.section.. Bool { + @MainActor func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, feedID: String, articleID: String) -> Bool { guard let itemIdentifierUserInfo = userInfo[UserInfoKey.itemIdentifier] as? [AnyHashable : AnyHashable], let itemIdentifier = ItemIdentifier(userInfo: itemIdentifierUserInfo), let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool, @@ -2271,7 +2273,7 @@ private extension SceneCoordinator { return nil } - func selectFeedAndArticle(itemIdentifier: ItemIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool { + @MainActor func selectFeedAndArticle(itemIdentifier: ItemIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool { guard let feedNode = nodeFor(itemID: itemIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false } selectFeed(indexPath: feedIndexPath) {