Fix numerous concurrency warnings.

This commit is contained in:
Brent Simmons 2024-03-19 20:33:54 -07:00
parent 186deebf9b
commit 6ab10e871c
4 changed files with 146 additions and 246 deletions

View File

@ -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)

View File

@ -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]()

View File

@ -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
}
}
}

View File

@ -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()
}
})
}