From 6ab10e871c85d10f08c5d84bef6394d752032322 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Tue, 19 Mar 2024 20:33:54 -0700 Subject: [PATCH] Fix numerous concurrency warnings. --- Shared/Activity/ActivityManager.swift | 13 +- Shared/IconImageCache.swift | 4 +- iOS/AppAssets.swift | 296 ++++++++------------------ iOS/AppDelegate.swift | 79 +++---- 4 files changed, 146 insertions(+), 246 deletions(-) diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index a3a90ea80..de11d53bc 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -113,7 +113,7 @@ class ActivityManager { } #if os(iOS) - static func cleanUp(_ account: Account) { + @MainActor static func cleanUp(_ account: Account) { Task { @MainActor in var ids = [String]() @@ -133,13 +133,15 @@ class ActivityManager { } } - static func cleanUp(_ folder: Folder) { + @MainActor static func cleanUp(_ folder: Folder) { Task { @MainActor in - var ids = [String]() + + var ids: [String] = [String]() ids.append(identifier(for: folder)) - for feed in folder.flattenedFeeds() { + let feeds = folder.flattenedFeeds() + for feed in feeds { let feedIdentifiers = await identifiers(for: feed) ids.append(contentsOf: feedIdentifiers) } @@ -148,7 +150,8 @@ class ActivityManager { } } - static func cleanUp(_ feed: Feed) { + @MainActor static func cleanUp(_ feed: Feed) { + Task { @MainActor in let feedIdentifiers = await identifiers(for: feed) try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: feedIdentifiers) diff --git a/Shared/IconImageCache.swift b/Shared/IconImageCache.swift index 3c32fbf9c..cf94d1b14 100644 --- a/Shared/IconImageCache.swift +++ b/Shared/IconImageCache.swift @@ -10,9 +10,9 @@ import Foundation import Account import Articles -class IconImageCache { +final class IconImageCache { - static var shared = IconImageCache() + static let shared = IconImageCache() private var smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]() private var feedIconImageCache = [SidebarItemIdentifier: IconImage]() diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 9c7d89180..3eb012dd1 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -11,248 +11,141 @@ import Account struct AppAssets { - static var accountBazQuxImage: UIImage = { - return UIImage(named: "accountBazQux")! - }() + static let accountBazQuxImage = UIImage(named: "accountBazQux")! - static var accountCloudKitImage: UIImage = { - return UIImage(named: "accountCloudKit")! - }() + static let accountCloudKitImage = UIImage(named: "accountCloudKit")! - static var accountFeedbinImage: UIImage = { - return UIImage(named: "accountFeedbin")! - }() + static let accountFeedbinImage = UIImage(named: "accountFeedbin")! - static var accountFeedlyImage: UIImage = { - return UIImage(named: "accountFeedly")! - }() - - static var accountFreshRSSImage: UIImage = { - return UIImage(named: "accountFreshRSS")! - }() + static let accountFeedlyImage = UIImage(named: "accountFeedly")! - static var accountInoreaderImage: UIImage = { - return UIImage(named: "accountInoreader")! - }() + static let accountFreshRSSImage = UIImage(named: "accountFreshRSS")! - static var accountLocalPadImage: UIImage = { - return UIImage(named: "accountLocalPad")! - }() + static let accountInoreaderImage = UIImage(named: "accountInoreader")! - static var accountLocalPhoneImage: UIImage = { - return UIImage(named: "accountLocalPhone")! - }() + static let accountLocalPadImage = UIImage(named: "accountLocalPad")! - static var accountNewsBlurImage: UIImage = { - return UIImage(named: "accountNewsBlur")! - }() + static let accountLocalPhoneImage = UIImage(named: "accountLocalPhone")! - static var accountTheOldReaderImage: UIImage = { - return UIImage(named: "accountTheOldReader")! - }() + static let accountNewsBlurImage = UIImage(named: "accountNewsBlur")! - static var articleExtractorError: UIImage = { - return UIImage(named: "articleExtractorError")! - }() + static let accountTheOldReaderImage = UIImage(named: "accountTheOldReader")! - static var articleExtractorOff: UIImage = { - return UIImage(named: "articleExtractorOff")! - }() + static let articleExtractorError = UIImage(named: "articleExtractorError")! - static var articleExtractorOffSF: UIImage = { - return UIImage(systemName: "doc.plaintext")! - }() + static let articleExtractorOff = UIImage(named: "articleExtractorOff")! - static var articleExtractorOffTinted: UIImage = { + static let articleExtractorOffSF = UIImage(systemName: "doc.plaintext")! + + static let articleExtractorOffTinted: UIImage = { let image = UIImage(named: "articleExtractorOff")! return image.tinted(color: AppAssets.primaryAccentColor)! }() - static var articleExtractorOn: UIImage = { - return UIImage(named: "articleExtractorOn")! - }() + static let articleExtractorOn = UIImage(named: "articleExtractorOn")! - static var articleExtractorOnSF: UIImage = { - return UIImage(named: "articleExtractorOnSF")! - }() + static let articleExtractorOnSF = UIImage(named: "articleExtractorOnSF")! - static var articleExtractorOnTinted: UIImage = { + static let articleExtractorOnTinted: UIImage = { let image = UIImage(named: "articleExtractorOn")! return image.tinted(color: AppAssets.primaryAccentColor)! }() - static var iconBackgroundColor: UIColor = { - return UIColor(named: "iconBackgroundColor")! - }() + static let iconBackgroundColor = UIColor(named: "iconBackgroundColor")! - static var circleClosedImage: UIImage = { - return UIImage(systemName: "largecircle.fill.circle")! - }() - - static var circleOpenImage: UIImage = { - return UIImage(systemName: "circle")! - }() - - static var disclosureImage: UIImage = { - return UIImage(named: "disclosure")! - }() - - static var copyImage: UIImage = { - return UIImage(systemName: "doc.on.doc")! - }() - - static var deactivateImage: UIImage = { - UIImage(systemName: "minus.circle")! - }() - - static var editImage: UIImage = { - UIImage(systemName: "square.and.pencil")! - }() - - static var faviconTemplateImage: RSImage = { - return RSImage(named: "faviconTemplateImage")! - }() - - static var filterInactiveImage: UIImage = { - UIImage(systemName: "line.horizontal.3.decrease.circle")! - }() - - static var filterActiveImage: UIImage = { - UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! - }() - - static var folderOutlinePlus: UIImage = { - UIImage(systemName: "folder.badge.plus")! - }() - - static var fullScreenBackgroundColor: UIColor = { - return UIColor(named: "fullScreenBackgroundColor")! - }() + static let circleClosedImage = UIImage(systemName: "largecircle.fill.circle")! - static var infoImage: UIImage = { - UIImage(systemName: "info.circle")! - }() + static let circleOpenImage = UIImage(systemName: "circle")! + + static let disclosureImage = UIImage(named: "disclosure")! + + static let copyImage = UIImage(systemName: "doc.on.doc")! + + static let deactivateImage = UIImage(systemName: "minus.circle")! + + static let editImage = UIImage(systemName: "square.and.pencil")! + + static let faviconTemplateImage = RSImage(named: "faviconTemplateImage")! + + static let filterInactiveImage = UIImage(systemName: "line.horizontal.3.decrease.circle")! - static var markAllAsReadImage: UIImage = { - return UIImage(named: "markAllAsRead")! - }() + static let filterActiveImage = UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! + + static let folderOutlinePlus = UIImage(systemName: "folder.badge.plus")! + + static let fullScreenBackgroundColor = UIColor(named: "fullScreenBackgroundColor")! + + static let infoImage = UIImage(systemName: "info.circle")! + + static let markAllAsReadImage = UIImage(named: "markAllAsRead")! + + static let markBelowAsReadImage = UIImage(systemName: "arrowtriangle.down.circle")! + + static let markAboveAsReadImage = UIImage(systemName: "arrowtriangle.up.circle")! + + static let folderImage = IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) + + static let folderImageNonIcon = UIImage(systemName: "folder.fill")!.withRenderingMode(.alwaysOriginal).withTintColor(.secondaryLabel) + + static let moreImage = UIImage(systemName: "ellipsis.circle")! + + static let nextArticleImage = UIImage(systemName: "chevron.down")! - static var markBelowAsReadImage: UIImage = { - return UIImage(systemName: "arrowtriangle.down.circle")! - }() - - static var markAboveAsReadImage: UIImage = { - return UIImage(systemName: "arrowtriangle.up.circle")! - }() - - static var folderImage: IconImage = { - return IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) - }() - - static var folderImageNonIcon: UIImage = { - return UIImage(systemName: "folder.fill")!.withRenderingMode(.alwaysOriginal).withTintColor(.secondaryLabel) - }() - - static var moreImage: UIImage = { - return UIImage(systemName: "ellipsis.circle")! - }() - - static var nextArticleImage: UIImage = { - return UIImage(systemName: "chevron.down")! - }() - - static var nextUnreadArticleImage: UIImage = { - return UIImage(systemName: "chevron.down.circle")! - }() - - static var plus: UIImage = { - UIImage(systemName: "plus")! - }() - - static var prevArticleImage: UIImage = { - return UIImage(systemName: "chevron.up")! - }() - - static var openInSidebarImage: UIImage = { - return UIImage(systemName: "arrow.turn.down.left")! - }() - - static var primaryAccentColor: UIColor { - return UIColor(named: "primaryAccentColor")! - } - - static var safariImage: UIImage = { - return UIImage(systemName: "safari")! - }() - - static var searchFeedImage: IconImage = { - return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) - }() - - static var secondaryAccentColor: UIColor { - return UIColor(named: "secondaryAccentColor")! - } - - static var sectionHeaderColor: UIColor = { - return UIColor(named: "sectionHeaderColor")! - }() - - static var shareImage: UIImage = { - return UIImage(systemName: "square.and.arrow.up")! - }() - - static var smartFeedImage: UIImage = { - return UIImage(systemName: "gear")! - }() - - static var starColor: UIColor = { - return UIColor(named: "starColor")! - }() - - static var starClosedImage: UIImage = { - return UIImage(systemName: "star.fill")! - }() - - static var starOpenImage: UIImage = { - return UIImage(systemName: "star")! - }() - - static var starredFeedImage: IconImage { + static let nextUnreadArticleImage = UIImage(systemName: "chevron.down.circle")! + + static let plus = UIImage(systemName: "plus")! + + static let prevArticleImage = UIImage(systemName: "chevron.up")! + + static let openInSidebarImage = UIImage(systemName: "arrow.turn.down.left")! + + static let primaryAccentColor = UIColor(named: "primaryAccentColor")! + + static let safariImage = UIImage(systemName: "safari")! + + static let searchFeedImage = IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) + + static let secondaryAccentColor = UIColor(named: "secondaryAccentColor")! + + static let sectionHeaderColor = UIColor(named: "sectionHeaderColor")! + + static let shareImage = UIImage(systemName: "square.and.arrow.up")! + + static let smartFeedImage = UIImage(systemName: "gear")! + + static let starColor = UIColor(named: "starColor")! + + static let starClosedImage = UIImage(systemName: "star.fill")! + + static let starOpenImage = UIImage(systemName: "star")! + + static let starredFeedImage: IconImage = { let image = UIImage(systemName: "star.fill")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.starColor.cgColor) - } - - static var tickMarkColor: UIColor = { - return UIColor(named: "tickMarkColor")! }() - - static var timelineStarImage: UIImage = { + + static let tickMarkColor = UIColor(named: "tickMarkColor")! + + static let timelineStarImage: UIImage = { let image = UIImage(systemName: "star.fill")! return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal) }() - static var todayFeedImage: IconImage { + static let todayFeedImage: IconImage = { let image = UIImage(systemName: "sun.max.fill")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: UIColor.systemOrange.cgColor) - } - - static var trashImage: UIImage = { - return UIImage(systemName: "trash")! }() - - static var unreadFeedImage: IconImage { + + static let trashImage = UIImage(systemName: "trash")! + + static let unreadFeedImage: IconImage = { let image = UIImage(systemName: "largecircle.fill.circle")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) - } - - static var vibrantTextColor: UIColor = { - return UIColor(named: "vibrantTextColor")! }() - static var controlBackgroundColor: UIColor = { - return UIColor(named: "controlBackgroundColor")! - }() + static let vibrantTextColor = UIColor(named: "vibrantTextColor")! + static let controlBackgroundColor = UIColor(named: "controlBackgroundColor")! static func image(for accountType: AccountType) -> UIImage? { switch accountType { @@ -280,5 +173,4 @@ struct AppAssets { return AppAssets.accountTheOldReaderImage } } - } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index f584fbef1..27f97f2d0 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -58,16 +58,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var isSyncArticleStatusRunning = false var isWaitingForSyncTasks = false + let accountManager: AccountManager + private var secretsProvider = Secrets() override init() { - super.init() - appDelegate = self let documentFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) - AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath, secretsProvider: secretsProvider) + self.accountManager = AccountManager(accountsFolder: documentAccountsFolderPath, secretsProvider: secretsProvider) + AccountManager.shared = accountManager + + super.init() + + appDelegate = self let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7))) @@ -85,8 +90,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD os_log("Is first run.", log: log, type: .info) } - if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { - let localAccount = AccountManager.shared.defaultAccount + if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() { + let localAccount = accountManager.defaultAccount DefaultFeedsImporter.importDefaultFeeds(account: localAccount) } @@ -95,13 +100,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD initializeDownloaders() initializeHomeScreenQuickActions() - DispatchQueue.main.async { - self.unreadCount = AccountManager.shared.unreadCount + Task { @MainActor in + self.unreadCount = accountManager.unreadCount } - + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in if granted { - DispatchQueue.main.async { + Task { @MainActor in UIApplication.shared.registerForRemoteNotifications() } } @@ -126,7 +131,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { DispatchQueue.main.async { self.resumeDatabaseProcessingIfNecessary() - AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { + self.accountManager.receiveRemoteNotification(userInfo: userInfo) { self.suspendApplication() completionHandler(.newData) } @@ -145,7 +150,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { - unreadCount = AccountManager.shared.unreadCount + unreadCount = accountManager.unreadCount } } @@ -159,12 +164,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach { $0.cleanUp(conditional: true) } - AccountManager.shared.refreshAll(errorHandler: errorHandler) + accountManager.refreshAll(errorHandler: errorHandler) } func resumeDatabaseProcessingIfNecessary() { - if AccountManager.shared.isSuspended { - AccountManager.shared.resumeAll() + if accountManager.isSuspended { + accountManager.resumeAll() os_log("Application processing resumed.", log: self.log, type: .info) } } @@ -183,12 +188,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if let lastRefresh = AppDefaults.shared.lastRefresh { if Date() > lastRefresh.addingTimeInterval(15 * 60) { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) + accountManager.refreshAll(errorHandler: ErrorHandler.log) } else { - AccountManager.shared.syncArticleStatusAll() + accountManager.syncArticleStatusAll() } } else { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) + accountManager.refreshAll(errorHandler: ErrorHandler.log) } } @@ -292,7 +297,7 @@ private extension AppDelegate { return } - if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning { + if accountManager.refreshInProgress || isSyncArticleStatusRunning { os_log("Waiting for sync to finish...", log: self.log, type: .info) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.waitToComplete(completion: completion) @@ -328,8 +333,8 @@ private extension AppDelegate { os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info) } - DispatchQueue.main.async { - AccountManager.shared.syncArticleStatusAll() { + Task { @MainActor in + self.accountManager.syncArticleStatusAll() { completeProcessing() } } @@ -338,8 +343,8 @@ private extension AppDelegate { func suspendApplication() { guard UIApplication.shared.applicationState == .background else { return } - AccountManager.shared.suspendNetworkAll() - AccountManager.shared.suspendDatabaseAll() + accountManager.suspendNetworkAll() + accountManager.suspendDatabaseAll() ArticleThemeDownloader.shared.cleanUp() CoalescingQueue.standard.performCallsImmediately() @@ -391,12 +396,12 @@ private extension AppDelegate { os_log("Woken to perform account refresh.", log: self.log, type: .info) - DispatchQueue.main.async { - if AccountManager.shared.isSuspended { - AccountManager.shared.resumeAll() + Task { @MainActor in + if self.accountManager.isSuspended { + self.accountManager.resumeAll() } - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in - if !AccountManager.shared.isSuspended { + self.accountManager.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in + if !self.accountManager.isSuspended { try? WidgetDataEncoder.shared.encodeWidgetData() self.suspendApplication() os_log("Account refresh operation completed.", log: self.log, type: .info) @@ -429,7 +434,7 @@ private extension AppDelegate { resumeDatabaseProcessingIfNecessary() - guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { + guard let account = accountManager.existingAccount(with: articlePathInfo.accountID) else { os_log(.debug, "No account found from notification.") return } @@ -445,11 +450,11 @@ private extension AppDelegate { self.prepareAccountsForBackground() - account.syncArticleStatus(completion: { [weak self] _ in - if !AccountManager.shared.isSuspended { + account.syncArticleStatus(completion: { _ in + if !self.accountManager.isSuspended { try? WidgetDataEncoder.shared.encodeWidgetData() - self?.prepareAccountsForBackground() - self?.suspendApplication() + self.prepareAccountsForBackground() + self.suspendApplication() } }) } @@ -463,7 +468,7 @@ private extension AppDelegate { resumeDatabaseProcessingIfNecessary() - guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { + guard let account = accountManager.existingAccount(with: articlePathInfo.accountID) else { os_log(.debug, "No account found from notification.") return } @@ -478,11 +483,11 @@ private extension AppDelegate { account.markArticles(articles, statusKey: .starred, flag: true) { _ in } - account.syncArticleStatus(completion: { [weak self] _ in - if !AccountManager.shared.isSuspended { + account.syncArticleStatus(completion: { _ in + if !self.accountManager.isSuspended { try? WidgetDataEncoder.shared.encodeWidgetData() - self?.prepareAccountsForBackground() - self?.suspendApplication() + self.prepareAccountsForBackground() + self.suspendApplication() } }) }