From d0760f3d129fb606b4c833353a0c7fa40e2e1d2b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Tue, 19 Mar 2024 23:05:30 -0700 Subject: [PATCH] Continue fixing concurrency warnings. --- Account/Sources/Account/Account.swift | 72 +++--- Account/Sources/Account/AccountManager.swift | 4 +- .../CloudKit/CloudKitArticlesZone.swift | 12 +- .../CloudKitSendStatusOperation.swift | 26 +- Account/Sources/Account/DataExtensions.swift | 4 +- .../OAuthAccountAuthorizationOperation.swift | 4 +- Mac/AppAssets.swift | 231 ++++++------------ Mac/AppDefaults.swift | 6 +- Mac/AppDelegate.swift | 119 +++++---- Mac/CrashReporter/CrashReporter.swift | 2 +- Mac/ErrorHandler.swift | 8 +- ...ltinSmartFeedInspectorViewController.swift | 2 +- .../FeedInspectorViewController.swift | 2 +- .../FolderInspectorViewController.swift | 2 +- Mac/Inspector/InspectorWindowController.swift | 8 +- .../NothingInspectorViewController.swift | 2 +- .../AddFeed/AddFeedController.swift | 2 +- Mac/MainWindow/AddFeed/FolderTreeMenu.swift | 2 +- .../Detail/DetailIconSchemeHandler.swift | 43 ++-- .../Detail/DetailWebViewController.swift | 69 ++++-- Mac/MainWindow/MainWindowController.swift | 15 +- .../NNW3/NNW3ImportController.swift | 2 +- .../Sidebar/Cell/SidebarCellLayout.swift | 2 +- .../Sidebar/SidebarDeleteItemsAlert.swift | 2 +- .../Sidebar/SidebarOutlineDataSource.swift | 2 +- .../Sidebar/SidebarViewController.swift | 29 +-- Mac/MainWindow/Sidebar/UnreadCountView.swift | 2 +- .../Timeline/ArticlePasteboardWriter.swift | 5 +- .../Cell/MultilineTextFieldSizer.swift | 4 +- .../Cell/SingleLineTextFieldSizer.swift | 2 +- .../Timeline/Cell/TimelineCellData.swift | 2 +- .../Timeline/Cell/TimelineCellLayout.swift | 2 +- .../TimelineContainerViewController.swift | 8 +- .../Timeline/TimelineViewController.swift | 16 +- .../Accounts/AddAccountsView.swift | 4 +- .../AppDelegate+Scriptability.swift | 2 +- Mac/Scriptability/Article+Scriptability.swift | 10 +- Mac/Scriptability/Feed+Scriptability.swift | 2 +- Mac/Scriptability/Folder+Scriptability.swift | 2 +- .../NSScriptCommand+NetNewsWire.swift | 2 +- Shared/Activity/ActivityManager.swift | 8 +- .../Article Extractor/ArticleExtractor.swift | 7 +- .../Article Rendering/ArticleRenderer.swift | 12 +- Shared/ArticlePathInfo.swift | 18 +- Shared/Commands/MarkStatusCommand.swift | 10 +- .../SendToMarsEditCommand.swift | 4 +- .../SendToMicroBlogCommand.swift | 4 +- .../Extensions/AddFeedDefaultContainer.swift | 2 +- .../Extensions/ArticleStringFormatter.swift | 2 +- Shared/Extensions/ArticleUtilities.swift | 16 +- Shared/Extensions/SmallIconProvider.swift | 2 +- Shared/Favicons/FaviconGenerator.swift | 2 +- Shared/IconImageCache.swift | 2 +- Shared/Importers/DefaultFeedsImporter.swift | 2 +- .../ExtensionContainersFile.swift | 4 +- .../ExtensionFeedAddRequestFile.swift | 18 +- Shared/SmartFeeds/SmartFeed.swift | 15 +- Shared/SmartFeeds/UnreadFeed.swift | 4 +- Shared/Timeline/ArticleArray.swift | 4 +- Shared/Timer/AccountRefreshTimer.swift | 4 +- Shared/Timer/ArticleStatusSyncTimer.swift | 4 +- Shared/Tree/FeedTreeControllerDelegate.swift | 6 +- .../Tree/FolderTreeControllerDelegate.swift | 6 +- .../UserNotificationManager.swift | 12 +- 64 files changed, 444 insertions(+), 459 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index bcbf97146..d8a213ff7 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -709,7 +709,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func articles(feed: Feed) async throws -> Set
{ let articles = try await database.articles(feedID: feed.feedID) - validateUnreadCount(feed, articles) + await validateUnreadCount(feed, articles) return articles } @@ -801,12 +801,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, precondition(type == .onMyMac || type == .cloudKit) database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in - switch updateArticlesResult { - case .success(let articleChanges): - self.sendNotificationAbout(articleChanges) - completion(.success(articleChanges)) - case .failure(let databaseError): - completion(.failure(databaseError)) + + MainActor.assumeIsolated { + switch updateArticlesResult { + case .success(let articleChanges): + self.sendNotificationAbout(articleChanges) + completion(.success(articleChanges)) + case .failure(let databaseError): + completion(.failure(databaseError)) + } } } } @@ -821,12 +824,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in - switch updateArticlesResult { - case .success(let newAndUpdatedArticles): - self.sendNotificationAbout(newAndUpdatedArticles) - completion(nil) - case .failure(let databaseError): - completion(databaseError) + + MainActor.assumeIsolated { + switch updateArticlesResult { + case .success(let newAndUpdatedArticles): + self.sendNotificationAbout(newAndUpdatedArticles) + completion(nil) + case .failure(let databaseError): + completion(databaseError) + } } } } @@ -839,14 +845,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } database.mark(articles, statusKey: statusKey, flag: flag) { result in - switch result { - case .success(let updatedStatuses): - let updatedArticleIDs = updatedStatuses.articleIDs() - let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) }) - self.noteStatusesForArticlesDidChange(updatedArticles) - completion(.success(updatedArticles)) - case .failure(let error): - completion(.failure(error)) + + MainActor.assumeIsolated { + switch result { + case .success(let updatedStatuses): + let updatedArticleIDs = updatedStatuses.articleIDs() + let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) }) + self.noteStatusesForArticlesDidChange(updatedArticles) + completion(.success(updatedArticles)) + case .failure(let error): + completion(.failure(error)) + } } } } @@ -1118,12 +1127,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)) + + MainActor.assumeIsolated { + switch articleSetResult { + case .success(let articles): + self?.validateUnreadCount(feed, articles) + completion(.success(articles)) + case .failure(let databaseError): + completion(.failure(databaseError)) + } } } } @@ -1227,7 +1239,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. @@ -1301,7 +1313,7 @@ private extension Account { unreadCount = updatedUnreadCount } - func noteStatusesForArticlesDidChange(_ articles: Set
) { + @MainActor func noteStatusesForArticlesDidChange(_ articles: Set
) { let feeds = Set(articles.compactMap { $0.feed }) let statuses = Set(articles.map { $0.status }) let articleIDs = Set(articles.map { $0.articleID }) @@ -1389,7 +1401,7 @@ private extension Account { } } - func sendNotificationAbout(_ articleChanges: ArticleChanges) { + @MainActor func sendNotificationAbout(_ articleChanges: ArticleChanges) { var feeds = Set() if let newArticles = articleChanges.newArticles { diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 37803307c..ae9d91904 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -18,7 +18,7 @@ import Secrets public final class AccountManager: UnreadCountProvider { - public static var shared: AccountManager! + @MainActor public static var shared: AccountManager! public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml" private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json" @@ -79,7 +79,7 @@ public final class AccountManager: UnreadCountProvider { return lastArticleFetchEndTime } - public func existingActiveAccount(forDisplayName displayName: String) -> Account? { + @MainActor public func existingActiveAccount(forDisplayName displayName: String) -> Account? { return AccountManager.shared.activeAccounts.first(where: { $0.nameForDisplay == displayName }) } diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index 87cc57740..13b148d2b 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -86,7 +86,7 @@ final class CloudKitArticlesZone: CloudKitZone { } } - func saveNewArticles(_ articles: Set
, completion: @escaping ((Result) -> Void)) { + @MainActor func saveNewArticles(_ articles: Set
, completion: @escaping ((Result) -> Void)) { guard !articles.isEmpty else { completion(.success(())) return @@ -112,7 +112,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 @@ -164,7 +164,7 @@ final class CloudKitArticlesZone: CloudKitZone { private extension CloudKitArticlesZone { - func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { + @MainActor func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { if case CloudKitZoneError.userDeletedZone = error { self.createZoneRecord() { result in switch result { @@ -187,7 +187,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 { @@ -198,7 +198,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) @@ -212,7 +212,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/CloudKitSendStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift index f2f5b70b1..5ef1efb0c 100644 --- a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -139,22 +139,24 @@ private extension CloudKitSendStatusOperation { return } } else { - articlesZone.modifyArticles(statusUpdates) { result in - switch result { - case .success: - self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in - done(false) - } - case .failure(let error): - self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in - self.processAccountError(account, error) - os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) - completion(true) + + Task { @MainActor in + articlesZone.modifyArticles(statusUpdates) { result in + switch result { + case .success: + self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in + done(false) + } + case .failure(let error): + self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in + self.processAccountError(account, error) + os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) + completion(true) + } } } } } - } switch result { diff --git a/Account/Sources/Account/DataExtensions.swift b/Account/Sources/Account/DataExtensions.swift index a503ea62d..12839815c 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/Feedly/OAuthAccountAuthorizationOperation.swift b/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift index c3f164370..8dcfc801c 100644 --- a/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift +++ b/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -133,7 +133,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { return anchor } - private func didEndRequestingAccessToken(_ result: Result) { + @MainActor private func didEndRequestingAccessToken(_ result: Result) { guard !isCanceled else { didFinish() return @@ -147,7 +147,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { } } - private func saveAccount(for grant: OAuthAuthorizationGrant) { + @MainActor private func saveAccount(for grant: OAuthAuthorizationGrant) { guard !AccountManager.shared.duplicateServiceAccount(type: .feedly, username: grant.accessToken.username) else { didFinish(OAuthAccountAuthorizationOperationError.duplicateAccount) return diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index 9b3ef4905..55db01efb 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -12,226 +12,144 @@ import Account struct AppAssets { - static var accountBazQux: RSImage! = { - return RSImage(named: "accountBazQux") - }() + static let accountBazQux = RSImage(named: "accountBazQux") - static var accountCloudKit: RSImage! = { - return RSImage(named: "accountCloudKit") - }() + static let accountCloudKit = RSImage(named: "accountCloudKit") - static var accountFeedbin: RSImage! = { - return RSImage(named: "accountFeedbin") - }() + static let accountFeedbin = RSImage(named: "accountFeedbin") + + static let accountFeedly = RSImage(named: "accountFeedly") - static var accountFeedly: RSImage! = { - return RSImage(named: "accountFeedly") - }() - - static var accountFreshRSS: RSImage! = { - return RSImage(named: "accountFreshRSS") - }() + static let accountFreshRSS = RSImage(named: "accountFreshRSS") - static var accountInoreader: RSImage! = { - return RSImage(named: "accountInoreader") - }() + static let accountInoreader = RSImage(named: "accountInoreader") - static var accountLocal: RSImage! = { - return RSImage(named: "accountLocal") - }() + static let accountLocal = RSImage(named: "accountLocal") - static var accountNewsBlur: RSImage! = { - return RSImage(named: "accountNewsBlur") - }() - - static var accountTheOldReader: RSImage! = { - return RSImage(named: "accountTheOldReader") - }() + static let accountNewsBlur = RSImage(named: "accountNewsBlur") - static var addNewSidebarItemImage: RSImage = { - return NSImage(systemSymbolName: "plus", accessibilityDescription: nil)! - }() + static let accountTheOldReader = RSImage(named: "accountTheOldReader") - static var articleExtractorError: RSImage = { - return RSImage(named: "articleExtractorError")! - }() + static let addNewSidebarItemImage = NSImage(systemSymbolName: "plus", accessibilityDescription: nil)! - static var articleExtractorOff: RSImage = { - return RSImage(named: "articleExtractorOff")! - }() + static let articleExtractorError = RSImage(named: "articleExtractorError")! - static var articleExtractorOn: RSImage = { - return RSImage(named: "articleExtractorOn")! - }() + static let articleExtractorOff = RSImage(named: "articleExtractorOff")! - static var articleTheme: RSImage = { - return NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)! - }() + static let articleExtractorOn = RSImage(named: "articleExtractorOn")! - static var cleanUpImage: RSImage = { - return NSImage(systemSymbolName: "wind", accessibilityDescription: nil)! - }() + static let articleTheme = NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)! - static var marsEditIcon: RSImage = { - return RSImage(named: "MarsEditIcon")! - }() - - static var microblogIcon: RSImage = { - return RSImage(named: "MicroblogIcon")! - }() - - static var faviconTemplateImage: RSImage = { - return RSImage(named: "faviconTemplateImage")! - }() + static let cleanUpImage = NSImage(systemSymbolName: "wind", accessibilityDescription: nil)! - static var filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)! + static let marsEditIcon = RSImage(named: "MarsEditIcon")! - static var filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)! + static let microblogIcon = RSImage(named: "MicroblogIcon")! - static var iconLightBackgroundColor: NSColor = { - return NSColor(named: NSColor.Name("iconLightBackgroundColor"))! - }() + static let faviconTemplateImage = RSImage(named: "faviconTemplateImage")! - static var iconDarkBackgroundColor: NSColor = { - return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))! - }() - - static var legacyArticleExtractor: RSImage! = { - return RSImage(named: "legacyArticleExtractor") - }() - - static var legacyArticleExtractorError: RSImage! = { - return RSImage(named: "legacyArticleExtractorError") - }() - - static var legacyArticleExtractorInactiveDark: RSImage! = { - return RSImage(named: "legacyArticleExtractorInactiveDark") - }() - - static var legacyArticleExtractorInactiveLight: RSImage! = { - return RSImage(named: "legacyArticleExtractorInactiveLight") - }() - - static var legacyArticleExtractorProgress1: RSImage! = { - return RSImage(named: "legacyArticleExtractorProgress1") - }() - - static var legacyArticleExtractorProgress2: RSImage! = { - return RSImage(named: "legacyArticleExtractorProgress2") - }() - - static var legacyArticleExtractorProgress3: RSImage! = { - return RSImage(named: "legacyArticleExtractorProgress3") - }() - - static var legacyArticleExtractorProgress4: RSImage! = { - return RSImage(named: "legacyArticleExtractorProgress4") - }() - - static var folderImage: IconImage = { + static let filterActive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle.fill", accessibilityDescription: nil)! + + static let filterInactive = NSImage(systemSymbolName: "line.horizontal.3.decrease.circle", accessibilityDescription: nil)! + + static let iconLightBackgroundColor = NSColor(named: NSColor.Name("iconLightBackgroundColor"))! + + static let iconDarkBackgroundColor = NSColor(named: NSColor.Name("iconDarkBackgroundColor"))! + + static let legacyArticleExtractor = RSImage(named: "legacyArticleExtractor")! + + static let legacyArticleExtractorError = RSImage(named: "legacyArticleExtractorError")! + + static let legacyArticleExtractorInactiveDark = RSImage(named: "legacyArticleExtractorInactiveDark")! + + static let legacyArticleExtractorInactiveLight = RSImage(named: "legacyArticleExtractorInactiveLight")! + + static let legacyArticleExtractorProgress1 = RSImage(named: "legacyArticleExtractorProgress1") + + static let legacyArticleExtractorProgress2 = RSImage(named: "legacyArticleExtractorProgress2") + + static let legacyArticleExtractorProgress3 = RSImage(named: "legacyArticleExtractorProgress3") + + static let legacyArticleExtractorProgress4 = RSImage(named: "legacyArticleExtractorProgress4") + + static let folderImage: IconImage = { let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)! let preferredColor = NSColor(named: "AccentColor")! let coloredImage = image.tinted(with: preferredColor) return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor) }() - static var markAllAsReadImage: RSImage = { - return RSImage(named: "markAllAsRead")! - }() + static let markAllAsReadImage = RSImage(named: "markAllAsRead")! - static var nextUnreadImage: RSImage = { - return NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)! - }() + static let nextUnreadImage = NSImage(systemSymbolName: "chevron.down.circle", accessibilityDescription: nil)! - static var openInBrowserImage: RSImage = { - return NSImage(systemSymbolName: "safari", accessibilityDescription: nil)! - }() + static let openInBrowserImage = NSImage(systemSymbolName: "safari", accessibilityDescription: nil)! - static var preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)! + static let preferencesToolbarAccountsImage = NSImage(systemSymbolName: "at", accessibilityDescription: nil)! - static var preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)! + static let preferencesToolbarGeneralImage = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)! - static var preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)! + static let preferencesToolbarAdvancedImage = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)! - static var readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)! + static let readClosedImage = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)! - static var readOpenImage: RSImage = { - return NSImage(systemSymbolName: "circle", accessibilityDescription: nil)! - }() + static let readOpenImage = NSImage(systemSymbolName: "circle", accessibilityDescription: nil)! - static var refreshImage: RSImage = { - return NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)! - }() - - static var searchFeedImage: IconImage = { + static let refreshImage = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)! + + static let searchFeedImage: IconImage = { return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSupressed: true) }() - static var shareImage: RSImage = { - return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)! - }() + static let shareImage = NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)! - static var sidebarToggleImage: RSImage = { - return NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)! - }() - - static var starClosedImage: RSImage = { - return NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)! - }() + static let sidebarToggleImage = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)! - static var starOpenImage: RSImage = { - return NSImage(systemSymbolName: "star", accessibilityDescription: nil)! - }() - - static var starredFeedImage: IconImage = { + static let starClosedImage = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)! + + static let starOpenImage = NSImage(systemSymbolName: "star", accessibilityDescription: nil)! + + static let starredFeedImage: IconImage = { let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)! let preferredColor = NSColor(named: "StarColor")! let coloredImage = image.tinted(with: preferredColor) return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor) }() - static var timelineSeparatorColor: NSColor = { - return NSColor(named: "timelineSeparatorColor")! - }() - - static var timelineStarSelected: RSImage! = { - return RSImage(named: "timelineStar")?.tinted(with: .white) - }() + static let timelineSeparatorColor = NSColor(named: "timelineSeparatorColor")! - static var timelineStarUnselected: RSImage! = { - return RSImage(named: "timelineStar")?.tinted(with: starColor) - }() + static let timelineStarSelected = RSImage(named: "timelineStar")?.tinted(with: .white) - static var todayFeedImage: IconImage = { + static let timelineStarUnselected = RSImage(named: "timelineStar")?.tinted(with: starColor) + + static let todayFeedImage: IconImage = { let image = NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)! let preferredColor = NSColor.orange let coloredImage = image.tinted(with: preferredColor) return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor) }() - static var unreadFeedImage: IconImage = { + static let unreadFeedImage: IconImage = { let image = NSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: nil)! let preferredColor = NSColor(named: "AccentColor")! let coloredImage = image.tinted(with: preferredColor) return IconImage(coloredImage, isSymbol: true, isBackgroundSupressed: true, preferredColor: preferredColor.cgColor) }() - static var swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")! + static let swipeMarkReadImage = RSImage(systemSymbolName: "circle", accessibilityDescription: "Mark Read")! .withSymbolConfiguration(.init(scale: .large)) - static var swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")! + static let swipeMarkUnreadImage = RSImage(systemSymbolName: "largecircle.fill.circle", accessibilityDescription: "Mark Unread")! .withSymbolConfiguration(.init(scale: .large)) - static var swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")! + static let swipeMarkStarredImage = RSImage(systemSymbolName: "star.fill", accessibilityDescription: "Star")! .withSymbolConfiguration(.init(scale: .large)) - static var swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")! + static let swipeMarkUnstarredImage = RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")! .withSymbolConfiguration(.init(scale: .large))! - static var starColor: NSColor = { - return NSColor(named: NSColor.Name("StarColor"))! - }() - + static let starColor = NSColor(named: NSColor.Name("StarColor"))! + static func image(for accountType: AccountType) -> NSImage? { switch accountType { case .onMyMac: @@ -254,5 +172,4 @@ struct AppAssets { return AppAssets.accountTheOldReader } } - } diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 72ba044ee..d827224a5 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -15,8 +15,8 @@ enum FontSize: Int { case veryLarge = 3 } -final class AppDefaults { - +final class AppDefaults: Sendable { + static let defaultThemeName = "Default" static let shared = AppDefaults() @@ -66,7 +66,7 @@ final class AppDefaults { return false }() - var isFirstRun: Bool = { + let isFirstRun: Bool = { if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date { return false } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 94fa565d4..3662afc2d 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -27,10 +27,10 @@ protocol SPUUpdaterDelegate {} import Sparkle #endif -var appDelegate: AppDelegate! +@MainActor var appDelegate: AppDelegate! @NSApplicationMain -final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate { +@MainActor final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate { private struct WindowRestorationIdentifiers { static let mainWindow = "mainWindow" @@ -109,9 +109,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat private var themeImportPath: String? private let secretsProvider = Secrets() + private let accountManager: AccountManager + private let articleThemesManager: ArticleThemesManager + + @MainActor override init() { - override init() { NSWindow.allowsAutomaticWindowTabbing = false + + self.accountManager = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider) + AccountManager.shared = self.accountManager + + self.articleThemesManager = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!) + ArticleThemesManager.shared = self.articleThemesManager + super.init() #if !MAC_APP_STORE @@ -120,9 +130,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat crashReporter.enable() #endif - AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider) - ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!) - NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) @@ -199,9 +206,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat if isFirstRun { os_log(.debug, "Is first run.") } - let localAccount = AccountManager.shared.defaultAccount + let localAccount = accountManager.defaultAccount - if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { + if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() { // Import feeds. Either old NNW 3 feeds or the default feeds. if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) { DefaultFeedsImporter.importDefaultFeeds(account: localAccount) @@ -223,8 +230,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) - DispatchQueue.main.async { - self.unreadCount = AccountManager.shared.unreadCount + Task { @MainActor in + self.unreadCount = self.accountManager.unreadCount } if InspectorWindowController.shouldOpenAtStartup { @@ -241,7 +248,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat UNUserNotificationCenter.current().getNotificationSettings { (settings) in if settings.authorizationStatus == .authorized { - DispatchQueue.main.async { + Task { @MainActor in NSApplication.shared.registerForRemoteNotifications() } } @@ -258,7 +265,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat refreshTimer!.update() syncTimer!.update() } else { - DispatchQueue.main.async { + Task { @MainActor in self.refreshTimer!.timedRefresh(nil) self.syncTimer!.timedRefresh(nil) } @@ -279,7 +286,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat } #if !MAC_APP_STORE - DispatchQueue.main.async { + Task { @MainActor in CrashReporter.check(crashReporter: self.crashReporter) } #endif @@ -318,7 +325,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat } func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { - AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) + accountManager.receiveRemoteNotification(userInfo: userInfo) } func application(_ sender: NSApplication, openFile filename: String) -> Bool { @@ -333,7 +340,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat ArticleThemeDownloader.shared.cleanUp() - AccountManager.shared.sendArticleStatusAll() { + accountManager.sendArticleStatusAll() { self.isShutDownSyncDone = true } @@ -344,7 +351,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat // MARK: Notifications @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { - unreadCount = AccountManager.shared.unreadCount + unreadCount = accountManager.unreadCount } } @@ -385,7 +392,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat let url = userInfo["url"] as? URL else { return } - DispatchQueue.main.async { + Task { @MainActor in self.importTheme(filename: url.path) } } @@ -444,15 +451,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false if item.action == #selector(refreshAll(_:)) { - return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty + return !accountManager.refreshInProgress && !accountManager.activeAccounts.isEmpty } if item.action == #selector(importOPMLFromFile(_:)) { - return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) + return accountManager.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) } if item.action == #selector(addAppNews(_:)) { - return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty + return !isDisplayingSheet && !accountManager.anyAccountHasNetNewsWireNewsSubscription() && !accountManager.activeAccounts.isEmpty } if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) { @@ -460,7 +467,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat } if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) { - return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty + return !isDisplayingSheet && !accountManager.activeAccounts.isEmpty } #if !MAC_APP_STORE @@ -474,23 +481,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat // MARK: UNUserNotificationCenterDelegate - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .badge, .sound]) } - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 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) + guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { + completionHandler() + return + } + + let actionIdentifier = response.actionIdentifier + + Task { @MainActor in + switch actionIdentifier { + case "MARK_AS_READ": + handleMarkAsRead(articlePathInfo: articlePathInfo) + case "MARK_AS_STARRED": + handleMarkAsStarred(articlePathInfo: articlePathInfo) + default: + mainWindowController?.handle(articlePathInfo: articlePathInfo) + } + completionHandler() } - completionHandler() } // MARK: Add Feed @@ -529,7 +544,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat } @IBAction func refreshAll(_ sender: Any?) { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) + accountManager.refreshAll(errorHandler: ErrorHandler.present) } @IBAction func showAddFeedWindow(_ sender: Any?) { @@ -603,7 +618,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat } @IBAction func addAppNews(_ sender: Any?) { - if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() { + if accountManager.anyAccountHasNetNewsWireNewsSubscription() { return } addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News") @@ -700,12 +715,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat extension AppDelegate { @IBAction func debugSearch(_ sender: Any?) { - AccountManager.shared.defaultAccount.debugRunSearch() + accountManager.defaultAccount.debugRunSearch() } @IBAction func debugDropConditionalGetInfo(_ sender: Any?) { #if DEBUG - AccountManager.shared.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() } + accountManager.activeAccounts.forEach{ $0.debugDropConditionalGetInfo() } #endif } @@ -817,7 +832,7 @@ internal extension AppDelegate { func importTheme() { do { - try ArticleThemesManager.shared.importTheme(filename: filename) + try articleThemesManager.importTheme(filename: filename) confirmImportSuccess(themeName: theme.name) } catch { NSApplication.shared.presentError(error) @@ -827,7 +842,7 @@ internal extension AppDelegate { alert.beginSheetModal(for: window) { result in if result == NSApplication.ModalResponse.alertFirstButtonReturn { - if ArticleThemesManager.shared.themeExists(filename: filename) { + if self.articleThemesManager.themeExists(filename: filename) { let alert = NSAlert() alert.alertStyle = .warning @@ -901,14 +916,14 @@ internal extension AppDelegate { informativeText = error.localizedDescription } - DispatchQueue.main.async { + Task { @MainActor in let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error") alert.informativeText = informativeText alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder")) alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) - + let button = alert.buttons.first button?.target = self button?.action = #selector(self.openThemesFolder(_:)) @@ -920,7 +935,7 @@ internal extension AppDelegate { @objc func openThemesFolder(_ sender: Any) { if themeImportPath == nil { - let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath) + let url = URL(fileURLWithPath: articleThemesManager.folderPath) NSWorkspace.shared.open(url) } else { let url = URL(fileURLWithPath: themeImportPath!) @@ -968,16 +983,15 @@ extension AppDelegate: NSWindowRestoration { private extension AppDelegate { - func handleMarkAsRead(userInfo: [AnyHashable: Any]) { + func handleMarkAsRead(articlePathInfo: ArticlePathInfo) { - guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { - return - } - guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { + guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else { os_log(.debug, "No account found from notification.") return } - let articleID = articlePathInfo.articleID + guard let articleID = articlePathInfo.articleID else { + return + } Task { guard let articles = try? await account.articles(for: .articleIDs([articleID])) else { @@ -989,16 +1003,15 @@ private extension AppDelegate { } } - func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { + func handleMarkAsStarred(articlePathInfo: ArticlePathInfo) { - guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { - return - } - guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { + guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else { os_log(.debug, "No account found from notification.") return } - let articleID = articlePathInfo.articleID + guard let articleID = articlePathInfo.articleID else { + return + } Task { diff --git a/Mac/CrashReporter/CrashReporter.swift b/Mac/CrashReporter/CrashReporter.swift index b37810a9d..09d1db06b 100644 --- a/Mac/CrashReporter/CrashReporter.swift +++ b/Mac/CrashReporter/CrashReporter.swift @@ -16,7 +16,7 @@ import CrashReporter // At some point this code should probably move into RSCore, so Rainier and any other // future apps can use it. -struct CrashReporter { +@MainActor struct CrashReporter { struct DefaultsKey { static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically" diff --git a/Mac/ErrorHandler.swift b/Mac/ErrorHandler.swift index 018da1a63..258712b5e 100644 --- a/Mac/ErrorHandler.swift +++ b/Mac/ErrorHandler.swift @@ -12,14 +12,14 @@ import os.log struct ErrorHandler { - private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account") - - public static func present(_ error: Error) { + private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account") + + @MainActor public static func present(_ error: Error) { NSApplication.shared.presentError(error) } public static func log(_ error: Error) { - os_log(.error, log: self.log, "%@", error.localizedDescription) + os_log(.error, log: log, "%@", error.localizedDescription) } } diff --git a/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift b/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift index 8433a8c98..ed5499b20 100644 --- a/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift +++ b/Mac/Inspector/BuiltinSmartFeedInspectorViewController.swift @@ -8,7 +8,7 @@ import AppKit -final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector { +@MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector { @IBOutlet var nameTextField: NSTextField? @IBOutlet weak var smartFeedImageView: NSImageView! diff --git a/Mac/Inspector/FeedInspectorViewController.swift b/Mac/Inspector/FeedInspectorViewController.swift index eabd77a11..ac25e29fa 100644 --- a/Mac/Inspector/FeedInspectorViewController.swift +++ b/Mac/Inspector/FeedInspectorViewController.swift @@ -11,7 +11,7 @@ import Articles import Account import UserNotifications -final class FeedInspectorViewController: NSViewController, Inspector { +@MainActor final class FeedInspectorViewController: NSViewController, Inspector { @IBOutlet weak var iconView: IconView! @IBOutlet weak var nameTextField: NSTextField? diff --git a/Mac/Inspector/FolderInspectorViewController.swift b/Mac/Inspector/FolderInspectorViewController.swift index 63f76b0d7..6e12f248d 100644 --- a/Mac/Inspector/FolderInspectorViewController.swift +++ b/Mac/Inspector/FolderInspectorViewController.swift @@ -10,7 +10,7 @@ import AppKit import Account import RSCore -final class FolderInspectorViewController: NSViewController, Inspector { +@MainActor final class FolderInspectorViewController: NSViewController, Inspector { @IBOutlet var nameTextField: NSTextField? @IBOutlet weak var folderImageView: NSImageView! diff --git a/Mac/Inspector/InspectorWindowController.swift b/Mac/Inspector/InspectorWindowController.swift index 2686f86c4..c0a3e5547 100644 --- a/Mac/Inspector/InspectorWindowController.swift +++ b/Mac/Inspector/InspectorWindowController.swift @@ -10,11 +10,11 @@ import AppKit protocol Inspector: AnyObject { - var objects: [Any]? { get set } - var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects. - var windowTitle: String { get } + @MainActor var objects: [Any]? { get set } + @MainActor var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects. + @MainActor var windowTitle: String { get } - func canInspect(_ objects: [Any]) -> Bool + @MainActor func canInspect(_ objects: [Any]) -> Bool } typealias InspectorViewController = Inspector & NSViewController diff --git a/Mac/Inspector/NothingInspectorViewController.swift b/Mac/Inspector/NothingInspectorViewController.swift index 376e6a594..168513e5e 100644 --- a/Mac/Inspector/NothingInspectorViewController.swift +++ b/Mac/Inspector/NothingInspectorViewController.swift @@ -8,7 +8,7 @@ import AppKit -final class NothingInspectorViewController: NSViewController, Inspector { +@MainActor final class NothingInspectorViewController: NSViewController, Inspector { @IBOutlet var nothingTextField: NSTextField? @IBOutlet var multipleTextField: NSTextField? diff --git a/Mac/MainWindow/AddFeed/AddFeedController.swift b/Mac/MainWindow/AddFeed/AddFeedController.swift index afc8916cb..1824ac925 100644 --- a/Mac/MainWindow/AddFeed/AddFeedController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedController.swift @@ -22,7 +22,7 @@ import RSParser // Else, // display error sheet. -class AddFeedController: AddFeedWindowControllerDelegate { +@MainActor final class AddFeedController: AddFeedWindowControllerDelegate { private let hostWindow: NSWindow private var addFeedWindowController: AddFeedWindowController? diff --git a/Mac/MainWindow/AddFeed/FolderTreeMenu.swift b/Mac/MainWindow/AddFeed/FolderTreeMenu.swift index 87250dd6b..1c696f0dc 100644 --- a/Mac/MainWindow/AddFeed/FolderTreeMenu.swift +++ b/Mac/MainWindow/AddFeed/FolderTreeMenu.swift @@ -11,7 +11,7 @@ import RSCore import RSTree import Account -class FolderTreeMenu { +@MainActor final class FolderTreeMenu { static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu { diff --git a/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift index 4aee30c11..ac9ba1354 100644 --- a/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift +++ b/Mac/MainWindow/Detail/DetailIconSchemeHandler.swift @@ -16,31 +16,32 @@ class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler { func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { - guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else { - urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) - return - } + Task { @MainActor in - let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) - iconView.iconImage = iconImage - let renderedImage = iconView.asImage() - - guard let data = renderedImage.dataRepresentation() else { - urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) - return - } - - let headerFields = ["Cache-Control": "no-cache"] - if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) { - urlSchemeTask.didReceive(response) - urlSchemeTask.didReceive(data) - urlSchemeTask.didFinish() - } + guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else { + urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) + return + } + let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) + iconView.iconImage = iconImage + let renderedImage = iconView.asImage() + + guard let data = renderedImage.dataRepresentation() else { + urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) + return + } + + let headerFields = ["Cache-Control": "no-cache"] + if let response = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: headerFields) { + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } + } } - + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { urlSchemeTask.didFailWithError(URLError(.unknown)) } - } diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index ba4cf19e5..3f3abf0ad 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -216,13 +216,26 @@ final class DetailWebViewController: NSViewController { extension DetailWebViewController: WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == MessageName.windowDidScroll { - windowScrollY = message.body as? CGFloat + + let updatedWindowScrollY = message.body as? CGFloat + Task { @MainActor in + windowScrollY = updatedWindowScrollY + } + } else if message.name == MessageName.mouseDidEnter, let link = message.body as? String { - delegate?.mouseDidEnter(self, link: link) + + Task { @MainActor in + delegate?.mouseDidEnter(self, link: link) + } + } else if message.name == MessageName.mouseDidExit { - delegate?.mouseDidExit(self) + + Task { @MainActor in + delegate?.mouseDidExit(self) + } } } } @@ -239,10 +252,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate { // WKNavigationDelegate - public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + nonisolated public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { if let url = navigationAction.request.url { - self.openInBrowser(url, flags: navigationAction.modifierFlags) + let flags = navigationAction.modifierFlags + Task { @MainActor in + self.openInBrowser(url, flags: flags) + } } decisionHandler(.cancel) return @@ -251,35 +267,42 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate { decisionHandler(.allow) } - public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // See note in viewDidLoad() - if waitingForFirstReload { - assert(webView.isHidden) - waitingForFirstReload = false - reloadHTML() + nonisolated public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // Waiting for the first navigation to complete isn't long enough to avoid the flash of white. - // A hard coded value is awful, but 5/100th of a second seems to be enough. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - webView.isHidden = false - } - } else { - if let windowScrollY = windowScrollY { - webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));") - self.windowScrollY = nil + Task { @MainActor in + // See note in viewDidLoad() + if waitingForFirstReload { + assert(webView.isHidden) + waitingForFirstReload = false + reloadHTML() + + // Waiting for the first navigation to complete isn't long enough to avoid the flash of white. + // A hard coded value is awful, but 5/100th of a second seems to be enough. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + webView.isHidden = false + } + } else { + if let windowScrollY = windowScrollY { + _ = try? await webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));") + self.windowScrollY = nil + } } } } // WKUIDelegate - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { // This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One // example where this is used is in YouTube's embedded video player when a user clicks on the video's title // or on the "Watch in YouTube" button. For our purposes we'll handle such window.open calls the same way we // handle clicks on a URL. if let url = navigationAction.request.url { - self.openInBrowser(url, flags: navigationAction.modifierFlags) + let flags = navigationAction.modifierFlags + + Task { @MainActor in + self.openInBrowser(url, flags: flags) + } } return nil diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 3ad3a140f..88452fede 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -115,16 +115,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { return sidebarViewController?.selectedObjects } - func handle(_ response: UNNotificationResponse) { - let userInfo = response.notification.request.content.userInfo - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return } - sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo) - currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo) + func handle(articlePathInfo: ArticlePathInfo) { + sidebarViewController?.deepLinkRevealAndSelect(for: articlePathInfo) + currentTimelineViewController?.goToDeepLink(for: articlePathInfo) } func handle(_ activity: NSUserActivity) { - guard let userInfo = activity.userInfo else { return } - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return } + + guard let userInfo = activity.userInfo, let articlePathUserInfo = ArticlePathInfo(userInfo: userInfo) else { + return + } + sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo) currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo) } diff --git a/Mac/MainWindow/NNW3/NNW3ImportController.swift b/Mac/MainWindow/NNW3/NNW3ImportController.swift index 9a6f4a977..152babdca 100644 --- a/Mac/MainWindow/NNW3/NNW3ImportController.swift +++ b/Mac/MainWindow/NNW3/NNW3ImportController.swift @@ -10,7 +10,7 @@ import AppKit import Account import UniformTypeIdentifiers -struct NNW3ImportController { +@MainActor struct NNW3ImportController { /// Import NNW3 subscriptions if they exist. /// Return true if Subscriptions.plist was found and subscriptions were imported. diff --git a/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift b/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift index e054b924a..ed9e91a94 100644 --- a/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift +++ b/Mac/MainWindow/Sidebar/Cell/SidebarCellLayout.swift @@ -11,7 +11,7 @@ import RSCore // image - title - unreadCount -struct SidebarCellLayout { +@MainActor struct SidebarCellLayout { let faviconRect: CGRect let titleRect: CGRect diff --git a/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift b/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift index 96d325044..59b20430a 100644 --- a/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift +++ b/Mac/MainWindow/Sidebar/SidebarDeleteItemsAlert.swift @@ -10,7 +10,7 @@ import AppKit import RSTree import Account -enum SidebarDeleteItemsAlert { +@MainActor struct SidebarDeleteItemsAlert { /// Builds a delete confirmation dialog for the supplied nodes static func build(_ nodes: [Node]) -> NSAlert { diff --git a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift index a07545fa5..76eac34eb 100644 --- a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -12,7 +12,7 @@ import Articles import RSCore import Account -@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource { +@objc @MainActor final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource { let treeController: TreeController static let dragOperationNone = NSDragOperation(rawValue: 0) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 2e5cb8226..934f21307 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -17,13 +17,13 @@ extension Notification.Name { } protocol SidebarDelegate: AnyObject { - func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?) - func unreadCount(for: AnyObject) -> Int - func sidebarInvalidatedRestorationState(_: SidebarViewController) + @MainActor func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?) + @MainActor func unreadCount(for: AnyObject) -> Int + @MainActor func sidebarInvalidatedRestorationState(_: SidebarViewController) } -@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner { - +@objc @MainActor class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner { + @IBOutlet weak var outlineView: NSOutlineView! weak var delegate: SidebarDelegate? @@ -483,9 +483,9 @@ protocol SidebarDelegate: AnyObject { revealAndSelectRepresentedObject(feed as AnyObject) } - func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) { - guard let accountNode = findAccountNode(userInfo), - let feedNode = findFeedNode(userInfo, beginningAt: accountNode), + func deepLinkRevealAndSelect(for articlePathInfo: ArticlePathInfo) { + guard let accountNode = findAccountNode(articlePathInfo), + let feedNode = findFeedNode(articlePathInfo, beginningAt: accountNode), let feed = feedNode.representedObject as? SidebarItem else { return } @@ -738,16 +738,17 @@ private extension SidebarViewController { return nil } - func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? { - guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else { + func findAccountNode(_ articlePathInfo: ArticlePathInfo) -> Node? { + + guard let accountID = articlePathInfo.accountID else { return nil } - + if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) { return node } - guard let accountName = userInfo?[ArticlePathKey.accountName] as? String else { + guard let accountName = articlePathInfo.accountName else { return nil } @@ -758,8 +759,8 @@ private extension SidebarViewController { return nil } - func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? { - guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else { + func findFeedNode(_ articlePathInfo: ArticlePathInfo, beginningAt startingNode: Node) -> Node? { + guard let feedID = articlePathInfo.feedID else { return nil } if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) { diff --git a/Mac/MainWindow/Sidebar/UnreadCountView.swift b/Mac/MainWindow/Sidebar/UnreadCountView.swift index 25ee1c2a4..ddf9aeb17 100644 --- a/Mac/MainWindow/Sidebar/UnreadCountView.swift +++ b/Mac/MainWindow/Sidebar/UnreadCountView.swift @@ -8,7 +8,7 @@ import AppKit -class UnreadCountView : NSView { +@MainActor final class UnreadCountView : NSView { struct Appearance { static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0) diff --git a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift index 427b2820d..612f05a45 100644 --- a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -11,12 +11,13 @@ import Articles import RSCore extension Article: PasteboardWriterOwner { - public var pasteboardWriter: NSPasteboardWriting { + + @MainActor public var pasteboardWriter: NSPasteboardWriting { return ArticlePasteboardWriter(article: self) } } -@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting { +@objc @MainActor final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting { let article: Article static let articleUTI = "com.ranchero.article" diff --git a/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift b/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift index 82f49370a..ececad259 100644 --- a/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift +++ b/Mac/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift @@ -26,7 +26,7 @@ struct TextFieldSizeInfo { let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then. } -final class MultilineTextFieldSizer { +@MainActor final class MultilineTextFieldSizer { private let numberOfLines: Int private let font: NSFont @@ -35,7 +35,7 @@ final class MultilineTextFieldSizer { private let doubleLineHeightEstimate: Int private var cache = [String: WidthHeightCache]() // Each string has a cache. private var attributedCache = [NSAttributedString: WidthHeightCache]() - private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]() + @MainActor private static var sizers = [TextFieldSizerSpecifier: MultilineTextFieldSizer]() private init(numberOfLines: Int, font: NSFont) { diff --git a/Mac/MainWindow/Timeline/Cell/SingleLineTextFieldSizer.swift b/Mac/MainWindow/Timeline/Cell/SingleLineTextFieldSizer.swift index e40db7235..f4557bdcc 100644 --- a/Mac/MainWindow/Timeline/Cell/SingleLineTextFieldSizer.swift +++ b/Mac/MainWindow/Timeline/Cell/SingleLineTextFieldSizer.swift @@ -12,7 +12,7 @@ import AppKit // Uses a cache. // Main thready only. -final class SingleLineTextFieldSizer { +@MainActor final class SingleLineTextFieldSizer { let font: NSFont private let textField: NSTextField diff --git a/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift b/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift index 53bb79adb..62634a9f6 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -9,7 +9,7 @@ import AppKit import Articles -struct TimelineCellData { +@MainActor struct TimelineCellData { private static let noText = NSLocalizedString("(No Text)", comment: "No Text") diff --git a/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift index df4fb42b2..21f8dcdef 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -9,7 +9,7 @@ import AppKit import RSCore -struct TimelineCellLayout { +@MainActor struct TimelineCellLayout { let width: CGFloat let height: CGFloat diff --git a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift index b589aa939..5d0e83340 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift @@ -11,10 +11,10 @@ import Account import Articles protocol TimelineContainerViewControllerDelegate: AnyObject { - func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) - func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed) - func timelineInvalidatedRestorationState(_: TimelineContainerViewController) - + + @MainActor func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) + @MainActor func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed) + @MainActor func timelineInvalidatedRestorationState(_: TimelineContainerViewController) } final class TimelineContainerViewController: NSViewController { diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 517ffb461..68bbe9470 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -13,9 +13,10 @@ import Account import os.log protocol TimelineDelegate: AnyObject { - func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?) - func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) - func timelineInvalidatedRestorationState(_: TimelineViewController) + + @MainActor func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?) + @MainActor func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) + @MainActor func timelineInvalidatedRestorationState(_: TimelineViewController) } enum TimelineShowFeedName { @@ -535,12 +536,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr // MARK: - Navigation - func goToDeepLink(for userInfo: [AnyHashable : Any]) { - guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return } + func goToDeepLink(for articlePathInfo: ArticlePathInfo) { + + guard let articleID = articlePathInfo.articleID else { + return + } Task { if isReadFiltered ?? false { - if let accountName = userInfo[ArticlePathKey.accountName] as? String, + if let accountName = articlePathInfo.accountName, let account = AccountManager.shared.existingActiveAccount(forDisplayName: accountName) { exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) await fetchAndReplaceArticles() diff --git a/Mac/Preferences/Accounts/AddAccountsView.swift b/Mac/Preferences/Accounts/AddAccountsView.swift index 1a143d5c8..b615327b2 100644 --- a/Mac/Preferences/Accounts/AddAccountsView.swift +++ b/Mac/Preferences/Accounts/AddAccountsView.swift @@ -161,7 +161,7 @@ struct AddAccountsView: View { } - var icloudAccount: some View { + @MainActor var icloudAccount: some View { VStack(alignment: .leading) { Text("iCloud") .font(.headline) @@ -260,7 +260,7 @@ struct AddAccountsView: View { } } - private func isCloudInUse() -> Bool { + @MainActor private func isCloudInUse() -> Bool { AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) } diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index b7c76de0f..5faaffc90 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -92,7 +92,7 @@ extension AppDelegate : AppDelegateAppleEvents { } class NetNewsWireCreateElementCommand : NSCreateCommand { - override func performDefaultImplementation() -> Any? { + @MainActor override func performDefaultImplementation() -> Any? { let classDescription = self.createClassDescription if (classDescription.className == "feed") { return ScriptableFeed.handleCreateElement(command:self) diff --git a/Mac/Scriptability/Article+Scriptability.swift b/Mac/Scriptability/Article+Scriptability.swift index 094a023a3..d7f8e0564 100644 --- a/Mac/Scriptability/Article+Scriptability.swift +++ b/Mac/Scriptability/Article+Scriptability.swift @@ -111,7 +111,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta return article.status.boolStatus(forKey:.read) } set { - markArticles([self.article], statusKey: .read, flag: newValue) + Task { @MainActor in + markArticles([self.article], statusKey: .read, flag: newValue) + } } } @@ -121,7 +123,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta return article.status.boolStatus(forKey:.starred) } set { - markArticles([self.article], statusKey: .starred, flag: newValue) + Task { @MainActor in + markArticles([self.article], statusKey: .starred, flag: newValue) + } } } @@ -142,7 +146,7 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta } @objc(feed) - var feed: ScriptableFeed? { + @MainActor var feed: ScriptableFeed? { guard let parentFeed = self.article.feed, let account = parentFeed.account else { return nil } diff --git a/Mac/Scriptability/Feed+Scriptability.swift b/Mac/Scriptability/Feed+Scriptability.swift index efecfd265..dc64995fc 100644 --- a/Mac/Scriptability/Feed+Scriptability.swift +++ b/Mac/Scriptability/Feed+Scriptability.swift @@ -81,7 +81,7 @@ import Articles } } - class func handleCreateElement(command:NSCreateCommand) -> Any? { + @MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? { guard command.isCreateCommand(forClass:"Feed") else { return nil } guard let arguments = command.arguments else {return nil} let titleFromArgs = command.property(forKey:"name") as? String diff --git a/Mac/Scriptability/Folder+Scriptability.swift b/Mac/Scriptability/Folder+Scriptability.swift index 61925db59..301dce64d 100644 --- a/Mac/Scriptability/Folder+Scriptability.swift +++ b/Mac/Scriptability/Folder+Scriptability.swift @@ -65,7 +65,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai or tell account X to make new folder at end with properties {name:"new folder name"} */ - class func handleCreateElement(command:NSCreateCommand) -> Any? { + @MainActor class func handleCreateElement(command:NSCreateCommand) -> Any? { guard command.isCreateCommand(forClass:"fold") else { return nil } let name = command.property(forKey:"name") as? String ?? "" diff --git a/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift b/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift index 1cc5e36e5..62311790b 100644 --- a/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift +++ b/Mac/Scriptability/NSScriptCommand+NetNewsWire.swift @@ -26,7 +26,7 @@ extension NSScriptCommand { return true } - func accountAndFolderForNewChild() -> (Account, Folder?) { + @MainActor func accountAndFolderForNewChild() -> (Account, Folder?) { let appleEvent = self.appleEvent var account = AccountManager.shared.defaultAccount var folder:Folder? = nil diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index de11d53bc..0ba584c49 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -20,8 +20,8 @@ import UniformTypeIdentifiers import CoreSpotlight #endif -class ActivityManager { - +@MainActor final class ActivityManager { + private var nextUnreadActivity: NSUserActivity? private var selectingActivity: NSUserActivity? private var readingActivity: NSUserActivity? @@ -264,8 +264,8 @@ 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(contentType: UTType.item) attributeSet.title = feed.nameForDisplay attributeSet.keywords = makeKeywords(feed.nameForDisplay) diff --git a/Shared/Article Extractor/ArticleExtractor.swift b/Shared/Article Extractor/ArticleExtractor.swift index 55108c149..b4ebb57f2 100644 --- a/Shared/Article Extractor/ArticleExtractor.swift +++ b/Shared/Article Extractor/ArticleExtractor.swift @@ -19,11 +19,12 @@ public enum ArticleExtractorState { } protocol ArticleExtractorDelegate { - func articleExtractionDidFail(with: Error) - func articleExtractionDidComplete(extractedArticle: ExtractedArticle) + + @MainActor func articleExtractionDidFail(with: Error) + @MainActor func articleExtractionDidComplete(extractedArticle: ExtractedArticle) } -class ArticleExtractor { +final class ArticleExtractor { private var dataTask: URLSessionDataTask? = nil diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 6bc71b48d..b6b7d3a40 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -14,7 +14,7 @@ import RSCore import Articles import Account -struct ArticleRenderer { +@MainActor struct ArticleRenderer { typealias Rendering = (style: String, html: String, title: String, baseURL: String) @@ -30,10 +30,10 @@ struct ArticleRenderer { } } - static var imageIconScheme = "nnwImageIcon" - - static var blank = Page(name: "blank") - static var page = Page(name: "page") + static let imageIconScheme = "nnwImageIcon" + + static let blank = Page(name: "blank") + static let page = Page(name: "page") private let article: Article? private let extractedArticle: ExtractedArticle? @@ -328,7 +328,7 @@ private extension ArticleRenderer { // MARK: - Article extension -private extension Article { +@MainActor private extension Article { var baseURL: URL? { var s = link diff --git a/Shared/ArticlePathInfo.swift b/Shared/ArticlePathInfo.swift index c1b5e0fb3..73c64941c 100644 --- a/Shared/ArticlePathInfo.swift +++ b/Shared/ArticlePathInfo.swift @@ -10,22 +10,20 @@ import Foundation struct ArticlePathInfo { - let accountID: String - let articleID: String + let accountID: String? + let accountName: String? + let articleID: String? + let feedID: String? init?(userInfo: [AnyHashable: Any]) { guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else { return nil } - guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else { - return nil - } - guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else { - return nil - } - self.accountID = accountID - self.articleID = articleID + self.accountID = articlePathUserInfo[ArticlePathKey.accountID] + self.accountName = articlePathUserInfo[ArticlePathKey.accountName] + self.articleID = articlePathUserInfo[ArticlePathKey.articleID] + self.feedID = articlePathUserInfo[ArticlePathKey.feedID] } } diff --git a/Shared/Commands/MarkStatusCommand.swift b/Shared/Commands/MarkStatusCommand.swift index 79cf9777c..999fe5a3f 100644 --- a/Shared/Commands/MarkStatusCommand.swift +++ b/Shared/Commands/MarkStatusCommand.swift @@ -12,7 +12,7 @@ import Articles // Mark articles read/unread, starred/unstarred, deleted/undeleted. -final class MarkStatusCommand: UndoableCommand { +@MainActor final class MarkStatusCommand: UndoableCommand { let undoActionName: String let redoActionName: String @@ -50,12 +50,12 @@ final class MarkStatusCommand: UndoableCommand { self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion) } - func perform() { + @MainActor func perform() { mark(statusKey, flag) registerUndo() } - func undo() { + @MainActor func undo() { mark(statusKey, !flag) registerRedo() } @@ -63,7 +63,7 @@ final class MarkStatusCommand: UndoableCommand { private extension MarkStatusCommand { - func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) { + @MainActor func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) { markArticles(articles, statusKey: statusKey, flag: flag, completion: completion) completion = nil } @@ -83,7 +83,7 @@ private extension MarkStatusCommand { } } - static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] { + @MainActor static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] { return articles.filter{ article in guard article.status.boolStatus(forKey: statusKey) != flag else { return false } diff --git a/Shared/ExtensionPoints/SendToMarsEditCommand.swift b/Shared/ExtensionPoints/SendToMarsEditCommand.swift index 89a867fff..926dda93f 100644 --- a/Shared/ExtensionPoints/SendToMarsEditCommand.swift +++ b/Shared/ExtensionPoints/SendToMarsEditCommand.swift @@ -21,7 +21,7 @@ final class SendToMarsEditCommand: SendToCommand { appToUse() != nil } - func sendObject(_ object: Any?, selectedText: String?) { + @MainActor func sendObject(_ object: Any?, selectedText: String?) { guard canSendObject(object, selectedText: selectedText) else { return @@ -39,7 +39,7 @@ final class SendToMarsEditCommand: SendToCommand { private extension SendToMarsEditCommand { - func send(_ article: Article, to app: UserApp) { + @MainActor func send(_ article: Article, to app: UserApp) { // App has already been launched. diff --git a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift index f458138d8..62139e980 100644 --- a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift +++ b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift @@ -29,7 +29,7 @@ final class SendToMicroBlogCommand: SendToCommand { return true } - func sendObject(_ object: Any?, selectedText: String?) { + @MainActor func sendObject(_ object: Any?, selectedText: String?) { guard canSendObject(object, selectedText: selectedText) else { return @@ -60,7 +60,7 @@ final class SendToMicroBlogCommand: SendToCommand { private extension Article { - var attributionString: String { + @MainActor var attributionString: String { // Feed name, or feed name + author name (if author is specified per-article). // Includes trailing space. diff --git a/Shared/Extensions/AddFeedDefaultContainer.swift b/Shared/Extensions/AddFeedDefaultContainer.swift index 6bf7fa880..930ddffca 100644 --- a/Shared/Extensions/AddFeedDefaultContainer.swift +++ b/Shared/Extensions/AddFeedDefaultContainer.swift @@ -11,7 +11,7 @@ import Account struct AddFeedDefaultContainer { - static var defaultContainer: Container? { + @MainActor static var defaultContainer: Container? { if let accountID = AppDefaults.shared.addFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) { if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) { diff --git a/Shared/Extensions/ArticleStringFormatter.swift b/Shared/Extensions/ArticleStringFormatter.swift index dac926b34..97c603eb5 100644 --- a/Shared/Extensions/ArticleStringFormatter.swift +++ b/Shared/Extensions/ArticleStringFormatter.swift @@ -10,7 +10,7 @@ import Foundation import Articles import RSParser -struct ArticleStringFormatter { +@MainActor struct ArticleStringFormatter { private static var feedNameCache = [String: String]() private static var titleCache = [String: String]() diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index 0f7272e18..cf1975718 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -13,7 +13,7 @@ import Account // These handle multiple accounts. -func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { +@MainActor func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { let d: [String: Set
] = accountAndArticlesDictionary(articles) @@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set
) -> [String: extension Article { - var feed: Feed? { + @MainActor var feed: Feed? { return account?.existingFeed(withFeedID: feedID) } @@ -98,7 +98,7 @@ extension Article { return datePublished ?? dateModified ?? status.dateArrived } - var isAvailableToMarkUnread: Bool { + @MainActor var isAvailableToMarkUnread: Bool { guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in switch behavior { case .disallowMarkAsUnreadAfterPeriod(let days): @@ -117,11 +117,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] @@ -138,7 +138,7 @@ extension Article { } } - func byline() -> String { + @MainActor func byline() -> String { guard let authors = authors ?? feed?.authors, !authors.isEmpty else { return "" } @@ -199,7 +199,7 @@ struct ArticlePathKey { extension Article { - public var pathUserInfo: [AnyHashable : Any] { + @MainActor public var pathUserInfo: [AnyHashable : Any] { return [ ArticlePathKey.accountID: accountID, ArticlePathKey.accountName: account?.nameForDisplay ?? "", @@ -214,7 +214,7 @@ extension Article { extension Article: SortableArticle { - var sortableName: String { + @MainActor var sortableName: String { return feed?.name ?? "" } diff --git a/Shared/Extensions/SmallIconProvider.swift b/Shared/Extensions/SmallIconProvider.swift index 86d34462f..ca71ba44d 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/Favicons/FaviconGenerator.swift b/Shared/Favicons/FaviconGenerator.swift index 7f6c2cd6f..b68277eef 100644 --- a/Shared/Favicons/FaviconGenerator.swift +++ b/Shared/Favicons/FaviconGenerator.swift @@ -10,7 +10,7 @@ import Foundation import RSCore import Account -final class FaviconGenerator { +@MainActor final class FaviconGenerator { private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage diff --git a/Shared/IconImageCache.swift b/Shared/IconImageCache.swift index cf94d1b14..9fa180af7 100644 --- a/Shared/IconImageCache.swift +++ b/Shared/IconImageCache.swift @@ -10,7 +10,7 @@ import Foundation import Account import Articles -final class IconImageCache { +@MainActor final class IconImageCache { static let shared = IconImageCache() diff --git a/Shared/Importers/DefaultFeedsImporter.swift b/Shared/Importers/DefaultFeedsImporter.swift index 8ed67d3b9..d02b2cec3 100644 --- a/Shared/Importers/DefaultFeedsImporter.swift +++ b/Shared/Importers/DefaultFeedsImporter.swift @@ -10,7 +10,7 @@ import Foundation import Account import RSCore -struct DefaultFeedsImporter { +@MainActor struct DefaultFeedsImporter { static func importDefaultFeeds(account: Account) { let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")! diff --git a/Shared/ShareExtension/ExtensionContainersFile.swift b/Shared/ShareExtension/ExtensionContainersFile.swift index 094a5360d..0e97a45d9 100644 --- a/Shared/ShareExtension/ExtensionContainersFile.swift +++ b/Shared/ShareExtension/ExtensionContainersFile.swift @@ -14,9 +14,9 @@ import Account final class ExtensionContainersFile { - private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile") + private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile") - private static var filePath: String = { + private static let filePath: String = { let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) return containerURL!.appendingPathComponent("extension_containers.plist").path diff --git a/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift b/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift index 6c70d5961..13152591d 100644 --- a/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift +++ b/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift @@ -10,8 +10,8 @@ import Foundation import os.log import Account -final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { - +final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Sendable { + private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile") private static let filePath: String = { @@ -30,7 +30,7 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { return operationQueue } - override init() { + @MainActor override init() { operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 @@ -41,14 +41,16 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { } func presentedItemDidChange() { - DispatchQueue.main.async { + Task { @MainActor in self.process() } } func resume() { NSFileCoordinator.addFilePresenter(self) - process() + Task { @MainActor in + process() + } } func suspend() { @@ -95,8 +97,8 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { private extension ExtensionFeedAddRequestFile { - func process() { - + @MainActor func process() { + let decoder = PropertyListDecoder() let encoder = PropertyListEncoder() encoder.outputFormat = .binary @@ -130,7 +132,7 @@ private extension ExtensionFeedAddRequestFile { requests?.forEach { processRequest($0) } } - func processRequest(_ request: ExtensionFeedAddRequest) { + @MainActor func processRequest(_ request: ExtensionFeedAddRequest) { var destinationAccountID: String? = nil switch request.destinationContainerID { case .account(let accountID): diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index 33f73b697..54817ca66 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -62,7 +62,7 @@ final class SmartFeed: PseudoFeed { } } - @objc func fetchUnreadCounts() { + @objc @MainActor func fetchUnreadCounts() { let activeAccounts = AccountManager.shared.activeAccounts // Remove any accounts that are no longer active or have been deleted @@ -113,15 +113,18 @@ private extension SmartFeed { func fetchUnreadCount(for account: Account) { delegate.fetchUnreadCount(for: account) { singleUnreadCountResult in - guard let accountUnreadCount = try? singleUnreadCountResult.get() else { - return + + MainActor.assumeIsolated { + guard let accountUnreadCount = try? singleUnreadCountResult.get() else { + return + } + self.unreadCounts[account.accountID] = accountUnreadCount + self.updateUnreadCount() } - self.unreadCounts[account.accountID] = accountUnreadCount - self.updateUnreadCount() } } - 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/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index c1aa25e28..f90843b01 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) { + @objc @MainActor func unreadCountDidChange(_ note: Notification) { assert(note.object is AppDelegate) unreadCount = appDelegate.unreadCount diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 104f06f8c..b86fcf2d0 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -67,7 +67,7 @@ extension Array where Element == Article { return false } - func anyArticleIsReadAndCanMarkUnread() -> Bool { + @MainActor func anyArticleIsReadAndCanMarkUnread() -> Bool { return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread } } @@ -95,7 +95,7 @@ extension Array where Element == Article { var i = 0 for article in self { let otherArticle = otherArticles[i] - if article.account != otherArticle.account || article.articleID != otherArticle.articleID { + if article.accountID != otherArticle.accountID || article.articleID != otherArticle.articleID { return false } i += 1 diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index 04c944a5a..ca38a81a9 100644 --- a/Shared/Timer/AccountRefreshTimer.swift +++ b/Shared/Timer/AccountRefreshTimer.swift @@ -9,7 +9,7 @@ import Foundation import Account -class AccountRefreshTimer { +@MainActor final class AccountRefreshTimer { var shuttingDown = false @@ -64,7 +64,7 @@ class AccountRefreshTimer { } - @objc func timedRefresh(_ sender: Timer?) { + @objc @MainActor func timedRefresh(_ sender: Timer?) { guard !shuttingDown else { return diff --git a/Shared/Timer/ArticleStatusSyncTimer.swift b/Shared/Timer/ArticleStatusSyncTimer.swift index 32e3cf604..b465fb552 100644 --- a/Shared/Timer/ArticleStatusSyncTimer.swift +++ b/Shared/Timer/ArticleStatusSyncTimer.swift @@ -9,8 +9,8 @@ import Foundation import Account -class ArticleStatusSyncTimer { - +@MainActor final class ArticleStatusSyncTimer { + private static let intervalSeconds = Double(120) var shuttingDown = false diff --git a/Shared/Tree/FeedTreeControllerDelegate.swift b/Shared/Tree/FeedTreeControllerDelegate.swift index 435d40e71..299929814 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) } @@ -41,7 +41,7 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate { private extension FeedTreeControllerDelegate { - func childNodesForRootNode(_ rootNode: Node) -> [Node]? { + @MainActor func childNodesForRootNode(_ rootNode: Node) -> [Node]? { var topLevelNodes = [Node]() let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) @@ -132,7 +132,7 @@ private extension FeedTreeControllerDelegate { return node } - func sortedAccountNodes(_ parent: Node) -> [Node] { + @MainActor func sortedAccountNodes(_ parent: Node) -> [Node] { let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in let accountNode = parent.existingOrNewChildNode(with: account) accountNode.canHaveChildNodes = true diff --git a/Shared/Tree/FolderTreeControllerDelegate.swift b/Shared/Tree/FolderTreeControllerDelegate.swift index 2c1e60cf2..4051a42e4 100644 --- a/Shared/Tree/FolderTreeControllerDelegate.swift +++ b/Shared/Tree/FolderTreeControllerDelegate.swift @@ -14,7 +14,7 @@ import Account final class FolderTreeControllerDelegate: TreeControllerDelegate { - func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { + @MainActor func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { return node.isRoot ? childNodesForRootNode(node) : childNodes(node) } @@ -22,8 +22,8 @@ final class FolderTreeControllerDelegate: TreeControllerDelegate { private extension FolderTreeControllerDelegate { - func childNodesForRootNode(_ node: Node) -> [Node]? { - + @MainActor func childNodesForRootNode(_ node: Node) -> [Node]? { + let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in let accountNode = Node(representedObject: account, parent: node) accountNode.canHaveChildNodes = true diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 97cb2b4b1..d9d3e0988 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -25,9 +25,11 @@ final class UserNotificationManager: NSObject { return } - for article in articles { - if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false { - sendNotification(feed: feed, article: article) + Task { @MainActor in + for article in articles { + if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false { + sendNotification(feed: feed, article: article) + } } } } @@ -53,7 +55,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 @@ -79,7 +81,7 @@ private extension UserNotificationManager { /// - feed: `Feed` /// - Returns: A `UNNotifcationAttachment` 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