mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Fix numerous concurrency warnings by marking things as Sendable or as MainActor.
This commit is contained in:
@@ -15,60 +15,65 @@ class FeedFinder {
|
||||
static func find(url: URL) async throws -> Set<FeedSpecifier> {
|
||||
|
||||
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<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
@MainActor static func find(url: URL, completion: @escaping (Result<Set<FeedSpecifier>, 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<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
@MainActor static func findFeedsInHTMLPage(htmlData: Data, urlString: String, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
// Feeds in the <head> section we automatically assume are feeds.
|
||||
// If there are none from the <head> section,
|
||||
// then possible feeds in <body> section are downloaded individually
|
||||
@@ -149,7 +154,7 @@ private extension FeedFinder {
|
||||
return data.isProbablyHTML
|
||||
}
|
||||
|
||||
static func downloadFeedSpecifiers(_ downloadFeedSpecifiers: Set<FeedSpecifier>, feedSpecifiers: [String: FeedSpecifier], completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
@MainActor static func downloadFeedSpecifiers(_ downloadFeedSpecifiers: Set<FeedSpecifier>, feedSpecifiers: [String: FeedSpecifier], completion: @escaping (Result<Set<FeedSpecifier>, 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) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedSpecifier: Hashable {
|
||||
struct FeedSpecifier: Hashable, Sendable {
|
||||
|
||||
enum Source: Int {
|
||||
case UserEntered = 0, HTMLHead, HTMLLink
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<FeedlyStream, Error>) -> ()) {
|
||||
@MainActor func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
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<FeedlyStreamIds, Error>) -> ()) {
|
||||
@MainActor func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
guard !isSuspended else {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(TransportError.suspended))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyResourceProviding {
|
||||
var resource: FeedlyResourceId { get }
|
||||
@MainActor var resource: FeedlyResourceId { get }
|
||||
}
|
||||
|
||||
extension FeedlyFeedResourceId: FeedlyResourceProviding {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyEntryIdentifierProviding: AnyObject {
|
||||
var entryIds: Set<String> { get }
|
||||
@MainActor var entryIds: Set<String> { 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<ParsedItem> { get }
|
||||
@MainActor var parsedItemProviderName: String { get }
|
||||
@MainActor var parsedEntries: Set<ParsedItem> { 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<URL>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Secrets
|
||||
import WidgetKit
|
||||
import Core
|
||||
|
||||
var appDelegate: AppDelegate!
|
||||
@MainActor var appDelegate: AppDelegate!
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {
|
||||
|
||||
Reference in New Issue
Block a user