diff --git a/Account/Sources/Account/FeedFinder/FeedFinder.swift b/Account/Sources/Account/FeedFinder/FeedFinder.swift index 3ea7803af..f2235f107 100644 --- a/Account/Sources/Account/FeedFinder/FeedFinder.swift +++ b/Account/Sources/Account/FeedFinder/FeedFinder.swift @@ -15,60 +15,65 @@ class FeedFinder { static func find(url: URL) async throws -> Set { try await withCheckedThrowingContinuation { continuation in - self.find(url: url) { result in - switch result { - case .success(let feedSpecifiers): - continuation.resume(returning: feedSpecifiers) - case .failure(let error): - continuation.resume(throwing: error) + Task { @MainActor in + self.find(url: url) { result in + switch result { + case .success(let feedSpecifiers): + continuation.resume(returning: feedSpecifiers) + case .failure(let error): + continuation.resume(throwing: error) + } } } } } - static func find(url: URL, completion: @escaping (Result, Error>) -> Void) { + @MainActor static func find(url: URL, completion: @escaping (Result, Error>) -> Void) { downloadAddingToCache(url) { (data, response, error) in - if response?.forcedStatusCode == 404 { - if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), urlComponents.host == "micro.blog" { - urlComponents.path = "\(urlComponents.path).json" - if let newURLString = urlComponents.url?.absoluteString { - let microblogFeedSpecifier = FeedSpecifier(title: nil, urlString: newURLString, source: .HTMLLink, orderFound: 1) - completion(.success(Set([microblogFeedSpecifier]))) + MainActor.assumeIsolated { + + if response?.forcedStatusCode == 404 { + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), urlComponents.host == "micro.blog" { + urlComponents.path = "\(urlComponents.path).json" + if let newURLString = urlComponents.url?.absoluteString { + let microblogFeedSpecifier = FeedSpecifier(title: nil, urlString: newURLString, source: .HTMLLink, orderFound: 1) + completion(.success(Set([microblogFeedSpecifier]))) + } + } else { + completion(.failure(AccountError.createErrorNotFound)) } - } else { - completion(.failure(AccountError.createErrorNotFound)) + return } - return + + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data, let response = response else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if !response.statusIsOK || data.isEmpty { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if FeedFinder.isFeed(data, url.absoluteString) { + let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered, orderFound: 1) + completion(.success(Set([feedSpecifier]))) + return + } + + if !FeedFinder.isHTML(data) { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + FeedFinder.findFeedsInHTMLPage(htmlData: data, urlString: url.absoluteString, completion: completion) } - - if let error = error { - completion(.failure(error)) - return - } - - guard let data = data, let response = response else { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - if !response.statusIsOK || data.isEmpty { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - if FeedFinder.isFeed(data, url.absoluteString) { - let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered, orderFound: 1) - completion(.success(Set([feedSpecifier]))) - return - } - - if !FeedFinder.isHTML(data) { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - FeedFinder.findFeedsInHTMLPage(htmlData: data, urlString: url.absoluteString, completion: completion) } } } @@ -87,7 +92,7 @@ private extension FeedFinder { } } - static func findFeedsInHTMLPage(htmlData: Data, urlString: String, completion: @escaping (Result, Error>) -> Void) { + @MainActor static func findFeedsInHTMLPage(htmlData: Data, urlString: String, completion: @escaping (Result, Error>) -> Void) { // Feeds in the section we automatically assume are feeds. // If there are none from the section, // then possible feeds in section are downloaded individually @@ -149,7 +154,7 @@ private extension FeedFinder { return data.isProbablyHTML } - static func downloadFeedSpecifiers(_ downloadFeedSpecifiers: Set, feedSpecifiers: [String: FeedSpecifier], completion: @escaping (Result, Error>) -> Void) { + @MainActor static func downloadFeedSpecifiers(_ downloadFeedSpecifiers: Set, feedSpecifiers: [String: FeedSpecifier], completion: @escaping (Result, Error>) -> Void) { var resultFeedSpecifiers = feedSpecifiers let group = DispatchGroup() @@ -160,15 +165,19 @@ private extension FeedFinder { } group.enter() - downloadUsingCache(url) { (data, response, error) in - if let data = data, let response = response, response.statusIsOK, error == nil { - if self.isFeed(data, downloadFeedSpecifier.urlString) { - addFeedSpecifier(downloadFeedSpecifier, feedSpecifiers: &resultFeedSpecifiers) + + Task { @MainActor in + downloadUsingCache(url) { (data, response, error) in + MainActor.assumeIsolated { + if let data = data, let response = response, response.statusIsOK, error == nil { + if self.isFeed(data, downloadFeedSpecifier.urlString) { + addFeedSpecifier(downloadFeedSpecifier, feedSpecifiers: &resultFeedSpecifiers) + } + } + group.leave() } } - group.leave() } - } group.notify(queue: DispatchQueue.main) { diff --git a/Account/Sources/Account/FeedFinder/FeedSpecifier.swift b/Account/Sources/Account/FeedFinder/FeedSpecifier.swift index 4487065bb..7429fb04d 100644 --- a/Account/Sources/Account/FeedFinder/FeedSpecifier.swift +++ b/Account/Sources/Account/FeedFinder/FeedSpecifier.swift @@ -8,7 +8,7 @@ import Foundation -struct FeedSpecifier: Hashable { +struct FeedSpecifier: Hashable, Sendable { enum Source: Int { case UserEntered = 0, HTMLHead, HTMLLink diff --git a/Account/Sources/Account/Feedbin/FeedbinDate.swift b/Account/Sources/Account/Feedbin/FeedbinDate.swift index 7d35b66d9..1937f7a25 100644 --- a/Account/Sources/Account/Feedbin/FeedbinDate.swift +++ b/Account/Sources/Account/Feedbin/FeedbinDate.swift @@ -10,7 +10,7 @@ import Foundation struct FeedbinDate { - public static var formatter: DateFormatter = { + public static let formatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" formatter.locale = Locale(identifier: "en_US") diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index 6b61237d5..c53cb3e87 100644 --- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -606,7 +606,7 @@ extension FeedlyAPICaller: FeedlyGetCollectionsService { extension FeedlyAPICaller: FeedlyGetStreamContentsService { - func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { + @MainActor func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) @@ -674,7 +674,7 @@ extension FeedlyAPICaller: FeedlyGetStreamContentsService { extension FeedlyAPICaller: FeedlyGetStreamIdsService { - func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { + @MainActor func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) diff --git a/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift b/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift index c58d25a28..f0e3c0439 100644 --- a/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift +++ b/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift @@ -9,7 +9,7 @@ import Foundation protocol FeedlyResourceProviding { - var resource: FeedlyResourceId { get } + @MainActor var resource: FeedlyResourceId { get } } extension FeedlyFeedResourceId: FeedlyResourceProviding { diff --git a/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift b/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift index a87fbc4ea..9801268ca 100644 --- a/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift +++ b/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift @@ -9,7 +9,7 @@ import Foundation protocol FeedlyEntryIdentifierProviding: AnyObject { - var entryIds: Set { get } + @MainActor var entryIds: Set { get } } final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding { @@ -19,11 +19,11 @@ final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding { self.entryIds = entryIds } - func addEntryIds(from provider: FeedlyEntryIdentifierProviding) { + @MainActor func addEntryIds(from provider: FeedlyEntryIdentifierProviding) { entryIds.formUnion(provider.entryIds) } - func addEntryIds(in articleIds: [String]) { + @MainActor func addEntryIds(in articleIds: [String]) { entryIds.formUnion(articleIds) } } diff --git a/Account/Sources/Account/Feedly/Models/FeedlyResourceId.swift b/Account/Sources/Account/Feedly/Models/FeedlyResourceId.swift index 9e20b7a93..c01fd8679 100644 --- a/Account/Sources/Account/Feedly/Models/FeedlyResourceId.swift +++ b/Account/Sources/Account/Feedly/Models/FeedlyResourceId.swift @@ -12,7 +12,7 @@ import Foundation protocol FeedlyResourceId { /// The resource Id from Feedly. - var id: String { get } + @MainActor var id: String { get } } /// The Feed Resource is documented here: https://developer.feedly.com/cloud/ diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift index 5d32d76ec..e46378009 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift @@ -9,7 +9,7 @@ import Foundation protocol FeedlyCheckpointOperationDelegate: AnyObject { - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) + @MainActor func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) } /// Let the delegate know an instance is executing. The semantics are up to the delegate. diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift index 4d2b7ba7b..fe996947b 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift @@ -11,12 +11,12 @@ import Parser import os.log protocol FeedlyEntryProviding { - var entries: [FeedlyEntry] { get } + @MainActor var entries: [FeedlyEntry] { get } } protocol FeedlyParsedItemProviding { - var parsedItemProviderName: String { get } - var parsedEntries: Set { get } + @MainActor var parsedItemProviderName: String { get } + @MainActor var parsedEntries: Set { get } } protocol FeedlyGetStreamContentsOperationDelegate: AnyObject { @@ -26,7 +26,7 @@ protocol FeedlyGetStreamContentsOperationDelegate: AnyObject { /// Get the stream content of a Collection from Feedly. final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding { - struct ResourceProvider: FeedlyResourceProviding { + @MainActor struct ResourceProvider: FeedlyResourceProviding { var resource: FeedlyResourceId } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift index b720a26be..adbb4a5a9 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift @@ -10,7 +10,7 @@ import Foundation import os.log protocol FeedlyFeedsAndFoldersProviding { - var feedsAndFolders: [([FeedlyFeed], Folder)] { get } + @MainActor var feedsAndFolders: [([FeedlyFeed], Folder)] { get } } /// Reflect Collections from Feedly as Folders. diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift index f55358a72..4042bc759 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift @@ -11,7 +11,7 @@ import Web import Core protocol FeedlyOperationDelegate: AnyObject { - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) + @MainActor func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) } /// Abstract base class for Feedly sync operations. diff --git a/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift index 40d6c76ec..2f22bd105 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift @@ -13,7 +13,7 @@ protocol FeedlySearchService: AnyObject { } protocol FeedlySearchOperationDelegate: AnyObject { - func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) + @MainActor func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) } /// Find one and only one feed for a given query (usually, a URL). diff --git a/Account/Sources/Account/LocalAccount/InitialFeedDownloader.swift b/Account/Sources/Account/LocalAccount/InitialFeedDownloader.swift index 712d6b2a9..49b4003d6 100644 --- a/Account/Sources/Account/LocalAccount/InitialFeedDownloader.swift +++ b/Account/Sources/Account/LocalAccount/InitialFeedDownloader.swift @@ -14,14 +14,14 @@ struct InitialFeedDownloader { static func download(_ url: URL) async -> ParsedFeed? { - await withCheckedContinuation { continuation in + await withCheckedContinuation { @MainActor continuation in self.download(url) { parsedFeed in continuation.resume(returning: parsedFeed) } } } - static func download(_ url: URL,_ completion: @escaping (_ parsedFeed: ParsedFeed?) -> Void) { + @MainActor static func download(_ url: URL,_ completion: @escaping (_ parsedFeed: ParsedFeed?) -> Void) { downloadUsingCache(url) { (data, response, error) in guard let data = data else { diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index b2f5d2a06..9f9823da7 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -133,14 +133,16 @@ final class FaviconDownloader { return favicon(with: faviconURL, homePageURL: url) } - findFaviconURLs(with: url) { (faviconURLs) in - if let faviconURLs = faviconURLs { - // If the site explicitly specifies favicon.ico, it will appear twice. - self.currentHomePageHasOnlyFaviconICO = faviconURLs.count == 1 + Task { @MainActor in + findFaviconURLs(with: url) { (faviconURLs) in + if let faviconURLs = faviconURLs { + // If the site explicitly specifies favicon.ico, it will appear twice. + self.currentHomePageHasOnlyFaviconICO = faviconURLs.count == 1 - if let firstIconURL = faviconURLs.first { - let _ = self.favicon(with: firstIconURL, homePageURL: url) - self.remainingFaviconURLs[url] = faviconURLs.dropFirst() + if let firstIconURL = faviconURLs.first { + let _ = self.favicon(with: firstIconURL, homePageURL: url) + self.remainingFaviconURLs[url] = faviconURLs.dropFirst() + } } } } @@ -197,7 +199,7 @@ private extension FaviconDownloader { static let localeForLowercasing = Locale(identifier: "en_US") - func findFaviconURLs(with homePageURL: String, _ completion: @escaping ([String]?) -> Void) { + @MainActor func findFaviconURLs(with homePageURL: String, _ completion: @escaping ([String]?) -> Void) { guard let url = URL(unicodeString: homePageURL) else { completion(nil) diff --git a/Shared/Favicons/FaviconURLFinder.swift b/Shared/Favicons/FaviconURLFinder.swift index cba8faf8c..7d7001b04 100644 --- a/Shared/Favicons/FaviconURLFinder.swift +++ b/Shared/Favicons/FaviconURLFinder.swift @@ -16,14 +16,14 @@ import UniformTypeIdentifiers struct FaviconURLFinder { /// Uniform types to ignore when finding favicon URLs. - static var ignoredTypes = [UTType.svg] + static let ignoredTypes = [UTType.svg] /// Finds favicon URLs in a web page. /// - Parameters: /// - homePageURL: The page to search. /// - completion: A closure called when the links have been found. /// - urls: An array of favicon URLs as strings. - static func findFaviconURLs(with homePageURL: String, _ completion: @escaping (_ urls: [String]?) -> Void) { + @MainActor static func findFaviconURLs(with homePageURL: String, _ completion: @escaping (_ urls: [String]?) -> Void) { guard let _ = URL(unicodeString: homePageURL) else { completion(nil) diff --git a/Shared/Favicons/SingleFaviconDownloader.swift b/Shared/Favicons/SingleFaviconDownloader.swift index 6fb13c9da..871e72889 100644 --- a/Shared/Favicons/SingleFaviconDownloader.swift +++ b/Shared/Favicons/SingleFaviconDownloader.swift @@ -78,23 +78,25 @@ private extension SingleFaviconDownloader { readFromDisk { (image) in - if let image = image { - self.diskStatus = .onDisk - self.iconImage = IconImage(image) - self.postDidLoadFaviconNotification() - return - } - - self.diskStatus = .notOnDisk - - self.downloadFavicon { (image) in - + MainActor.assumeIsolated { if let image = image { + self.diskStatus = .onDisk self.iconImage = IconImage(image) + self.postDidLoadFaviconNotification() + return } - - self.postDidLoadFaviconNotification() + self.diskStatus = .notOnDisk + + self.downloadFavicon { (image) in + + if let image = image { + self.iconImage = IconImage(image) + } + + self.postDidLoadFaviconNotification() + + } } } } @@ -133,7 +135,7 @@ private extension SingleFaviconDownloader { } } - func downloadFavicon(_ completion: @escaping (RSImage?) -> Void) { + @MainActor func downloadFavicon(_ completion: @escaping (RSImage?) -> Void) { guard let url = URL(string: faviconURL) else { completion(nil) diff --git a/Shared/HTMLMetadata/HTMLMetadataDownloader.swift b/Shared/HTMLMetadata/HTMLMetadataDownloader.swift index 1ea8318db..910d47033 100644 --- a/Shared/HTMLMetadata/HTMLMetadataDownloader.swift +++ b/Shared/HTMLMetadata/HTMLMetadataDownloader.swift @@ -14,7 +14,7 @@ struct HTMLMetadataDownloader { static let serialDispatchQueue = DispatchQueue(label: "HTMLMetadataDownloader") - static func downloadMetadata(for url: String, _ completion: @escaping (RSHTMLMetadata?) -> Void) { + @MainActor static func downloadMetadata(for url: String, _ completion: @escaping (RSHTMLMetadata?) -> Void) { guard let actualURL = URL(unicodeString: url) else { completion(nil) return diff --git a/Shared/Images/ImageDownloader.swift b/Shared/Images/ImageDownloader.swift index dd1d15ece..2031e4a01 100644 --- a/Shared/Images/ImageDownloader.swift +++ b/Shared/Images/ImageDownloader.swift @@ -104,22 +104,24 @@ private extension ImageDownloader { return } - downloadUsingCache(imageURL) { (data, response, error) in - - if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { - self.saveToDisk(url, data) - completion(data) - return + Task { @MainActor in + downloadUsingCache(imageURL) { (data, response, error) in + + if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { + self.saveToDisk(url, data) + completion(data) + return + } + + if let response = response as? HTTPURLResponse, response.statusCode >= HTTPResponseCode.badRequest && response.statusCode <= HTTPResponseCode.notAcceptable { + self.badURLs.insert(url) + } + if let error = error { + os_log(.info, log: self.log, "Error downloading image at %@: %@.", url, error.localizedDescription) + } + + completion(nil) } - - if let response = response as? HTTPURLResponse, response.statusCode >= HTTPResponseCode.badRequest && response.statusCode <= HTTPResponseCode.notAcceptable { - self.badURLs.insert(url) - } - if let error = error { - os_log(.info, log: self.log, "Error downloading image at %@: %@.", url, error.localizedDescription) - } - - completion(nil) } } diff --git a/Web/Sources/Web/OneShotDownload.swift b/Web/Sources/Web/OneShotDownload.swift index cfe30d6d1..dc03d786f 100755 --- a/Web/Sources/Web/OneShotDownload.swift +++ b/Web/Sources/Web/OneShotDownload.swift @@ -10,9 +10,9 @@ import Foundation // Main thread only. -public typealias OneShotDownloadCallback = (Data?, URLResponse?, Error?) -> Swift.Void +public typealias OneShotDownloadCallback = @Sendable (Data?, URLResponse?, Error?) -> Swift.Void -private final class OneShotDownloadManager { +@MainActor private final class OneShotDownloadManager { private let urlSession: URLSession fileprivate static let shared = OneShotDownloadManager() @@ -61,12 +61,12 @@ private final class OneShotDownloadManager { // Call one of these. It’s easier than referring to OneShotDownloadManager. // callback is called on the main queue. -public func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback) { +@MainActor public func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback) { precondition(Thread.isMainThread) OneShotDownloadManager.shared.download(url, completion) } -public func download(_ urlRequest: URLRequest, _ completion: @escaping OneShotDownloadCallback) { +@MainActor public func download(_ urlRequest: URLRequest, _ completion: @escaping OneShotDownloadCallback) { precondition(Thread.isMainThread) OneShotDownloadManager.shared.download(urlRequest, completion) } @@ -136,7 +136,7 @@ private final class DownloadWithCacheManager { private var pendingCallbacks = [CallbackRecord]() private var urlsInProgress = Set() - func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback, forceRedownload: Bool = false) { + @MainActor func download(_ url: URL, _ completion: @escaping OneShotDownloadCallback, forceRedownload: Bool = false) { if lastCleanupDate.timeIntervalSinceNow < -DownloadWithCacheManager.cleanupInterval { lastCleanupDate = Date() @@ -160,33 +160,35 @@ private final class DownloadWithCacheManager { OneShotDownloadManager.shared.download(url) { (data, response, error) in - self.urlsInProgress.remove(url) + MainActor.assumeIsolated { + self.urlsInProgress.remove(url) - if let data = data, let response = response, response.statusIsOK, error == nil { - let cacheRecord = WebCacheRecord(url: url, dateDownloaded: Date(), data: data, response: response) - self.cache[url] = cacheRecord - } - - var callbackCount = 0 - self.pendingCallbacks.forEach{ (callbackRecord) in - if url == callbackRecord.url { - callbackRecord.completion(data, response, error) - callbackCount += 1 + if let data = data, let response = response, response.statusIsOK, error == nil { + let cacheRecord = WebCacheRecord(url: url, dateDownloaded: Date(), data: data, response: response) + self.cache[url] = cacheRecord } + + var callbackCount = 0 + self.pendingCallbacks.forEach{ (callbackRecord) in + if url == callbackRecord.url { + callbackRecord.completion(data, response, error) + callbackCount += 1 + } + } + self.pendingCallbacks.removeAll(where: { (callbackRecord) -> Bool in + return callbackRecord.url == url + }) } - self.pendingCallbacks.removeAll(where: { (callbackRecord) -> Bool in - return callbackRecord.url == url - }) } } } -public func downloadUsingCache(_ url: URL, _ completion: @escaping OneShotDownloadCallback) { +@MainActor public func downloadUsingCache(_ url: URL, _ completion: @escaping OneShotDownloadCallback) { precondition(Thread.isMainThread) DownloadWithCacheManager.shared.download(url, completion) } -public func downloadAddingToCache(_ url: URL, _ completion: @escaping OneShotDownloadCallback) { +@MainActor public func downloadAddingToCache(_ url: URL, _ completion: @escaping OneShotDownloadCallback) { precondition(Thread.isMainThread) DownloadWithCacheManager.shared.download(url, completion, forceRedownload: true) } diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 60d26cc61..5a1a9971e 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -84,7 +84,7 @@ struct AppAssets { static let markAboveAsReadImage = UIImage(systemName: "arrowtriangle.up.circle")! - static let folderImage = IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) + @MainActor 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) @@ -104,7 +104,7 @@ struct AppAssets { static let safariImage = UIImage(systemName: "safari")! - static let searchFeedImage = IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) + @MainActor static let searchFeedImage = IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) static let secondaryAccentColor = UIColor(named: "secondaryAccentColor")! @@ -120,7 +120,7 @@ struct AppAssets { static let starOpenImage = UIImage(systemName: "star")! - static let starredFeedImage: IconImage = { + @MainActor static let starredFeedImage: IconImage = { let image = UIImage(systemName: "star.fill")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.starColor.cgColor) }() @@ -132,14 +132,14 @@ struct AppAssets { return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal) }() - static let todayFeedImage: IconImage = { + @MainActor static let todayFeedImage: IconImage = { let image = UIImage(systemName: "sun.max.fill")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: UIColor.systemOrange.cgColor) }() static let trashImage = UIImage(systemName: "trash")! - static let unreadFeedImage: IconImage = { + @MainActor static let unreadFeedImage: IconImage = { let image = UIImage(systemName: "largecircle.fill.circle")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) }() @@ -148,7 +148,7 @@ struct AppAssets { static let controlBackgroundColor = UIColor(named: "controlBackgroundColor")! - static func image(for accountType: AccountType) -> UIImage? { + @MainActor static func image(for accountType: AccountType) -> UIImage? { switch accountType { case .onMyMac: if UIDevice.current.userInterfaceIdiom == .pad { diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 119fbfaa7..a426bcdeb 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -15,7 +15,7 @@ import Secrets import WidgetKit import Core -var appDelegate: AppDelegate! +@MainActor var appDelegate: AppDelegate! @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {