Compare commits

...

35 Commits

Author SHA1 Message Date
Andy Williams 87ec4fc4e0
Merge 1f1bbc8b26 into 178cba34ad 2024-05-06 09:49:41 +09:00
Brent Simmons 178cba34ad Turn on strict concurrency for Mac targets. 2024-05-05 16:43:38 -07:00
Brent Simmons 30e961bfe4 Fix warning about unused @preconcurrency. 2024-05-04 15:19:48 -07:00
Brent Simmons bf02d1d86a Use targeted concurrency for the app. Remaining concurrency warnings will probably have to wait for future updates from Apple. 2024-05-04 15:19:30 -07:00
Brent Simmons 16cebcd60a Fix some concurrency warnings. 2024-05-04 11:05:45 -07:00
Brent Simmons 7f042b5d07 Fix concurrency warning. 2024-05-03 23:17:54 -07:00
Brent Simmons 19a39ac295 Make TimelineTitleView final. 2024-05-03 23:17:04 -07:00
Brent Simmons 3f8724c9d1 Silence concurrency warning. 2024-05-03 23:16:55 -07:00
Brent Simmons 1e80253018 Fix some concurrency warnings. 2024-05-03 23:10:57 -07:00
Brent Simmons 83298770c2 Fix concurrency warning. 2024-05-03 23:04:13 -07:00
Brent Simmons 7f545c5a23 Fix some concurrency warnings. 2024-05-03 23:03:31 -07:00
Brent Simmons db9a8c7feb Fix some concurrency warnings. 2024-05-03 23:01:35 -07:00
Brent Simmons 4ea66ee11e Fix some concurrency warnings. 2024-05-03 22:57:10 -07:00
Brent Simmons ea0a827024 Fix some concurrency warnings. 2024-05-03 22:56:42 -07:00
Brent Simmons fdbcf0d84e Fix concurrency warning. 2024-05-03 22:55:29 -07:00
Brent Simmons 7d04021415 Fix some concurrency warnings. 2024-05-03 22:49:27 -07:00
Brent Simmons 87fe78f598 Fix concurrency warning. 2024-05-03 22:47:14 -07:00
Brent Simmons 9dcfc2b09c Fix concurrency warning. 2024-05-03 22:35:20 -07:00
Brent Simmons 22d184c5f6 Fix concurrency warning. 2024-05-03 22:33:21 -07:00
Brent Simmons 09cf212057 Fix some concurrency warnings. 2024-05-03 22:32:48 -07:00
Brent Simmons 07091e0d3e Fix concurrency warning. 2024-05-03 22:28:26 -07:00
Brent Simmons 0a2b4f7008 Silence concurrency warning. 2024-05-03 22:25:14 -07:00
Brent Simmons 9403d81550 Fix concurrency warning. 2024-05-03 22:24:07 -07:00
Brent Simmons 78a64c3146 Make WebViewController fina. 2024-05-03 22:23:55 -07:00
Brent Simmons 18d0b0e1e7 Fix concurrency warning in SendToCommand. 2024-05-03 22:05:35 -07:00
Brent Simmons 2418076364 Fix some concurrency warnings. 2024-05-03 22:00:35 -07:00
Brent Simmons a9d50f3a14 Make some things private in UserApp. 2024-05-03 21:40:46 -07:00
Brent Simmons 02d8005fa7 Fix a couple concurrency warnings. 2024-05-03 12:10:59 -07:00
Brent Simmons 19fd3d96ab Fix a few concurrency warnings. 2024-05-03 12:05:53 -07:00
Brent Simmons 81cede769a Fix a few concurrency warnings. 2024-05-03 11:57:20 -07:00
Brent Simmons 6776862322 Fix concurrency warnings in ShareViewController. 2024-05-03 11:45:59 -07:00
Brent Simmons 6c1ea427af Fix concurrency warnings about gPingPongMap. 2024-05-03 11:12:15 -07:00
Brent Simmons 5c31993b90 Fix concurrency warning. 2024-05-03 10:27:27 -07:00
Brent Simmons 325f8061de Fix a few concurrency warnings. 2024-05-03 09:56:51 -07:00
Andy Williams 1f1bbc8b26 Change toolbar button ID to the correct one 2024-03-14 20:57:04 -04:00
43 changed files with 219 additions and 171 deletions

View File

@ -995,7 +995,8 @@ public enum FetchType {
// MARK: - AccountMetadataDelegate
extension Account: AccountMetadataDelegate {
func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) {
nonisolated func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) {
Task { @MainActor in
metadataFile.markAsDirty()
}
@ -1006,11 +1007,13 @@ extension Account: AccountMetadataDelegate {
extension Account: FeedMetadataDelegate {
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
nonisolated func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
let feedID = feedMetadata.feedID
Task { @MainActor in
feedMetadataFile.markAsDirty()
guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else {
guard let feed = existingFeed(withFeedID: feedID) else {
return
}
feed.postFeedSettingDidChangeNotification(key)

View File

@ -24,7 +24,6 @@ final class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
weak var account: Account?
var database: SyncDatabase
weak var articlesZone: CloudKitArticlesZone?
var compressionQueue = DispatchQueue(label: "Articles Zone Delegate Compression Queue")
init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone) {
self.account = account
@ -42,13 +41,13 @@ final class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
await self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs)
self.update(records: changed,
try await self.update(records: changed,
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
completion: completion)
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs)
completion(.success(()))
} catch {
os_log(.error, log: self.log, "Error occurred getting pending status records: %@", error.localizedDescription)
os_log(.error, log: self.log, "Error in CloudKitArticlesZoneDelegate.cloudKitDidModify: %@", error.localizedDescription)
completion(.failure(CloudKitZoneError.unknown))
}
}
@ -57,7 +56,7 @@ final class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
private extension CloudKitArticlesZoneDelegate {
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>) async {
@MainActor func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>) async {
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
let receivedArticleIDs = Set(receivedRecordIDs.map({ stripPrefix($0.externalID) }))
@ -71,7 +70,7 @@ private extension CloudKitArticlesZoneDelegate {
try? await account?.delete(articleIDs: deletableArticleIDs)
}
@MainActor func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor private func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>) async throws {
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ stripPrefix($0.externalID) }))
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ stripPrefix($0.externalID) }))
@ -82,87 +81,76 @@ private extension CloudKitArticlesZoneDelegate {
let updateableReadArticleIDs = receivedReadArticleIDs.subtracting(pendingReadStatusArticleIDs)
let updateableUnstarredArticleIDs = receivedUnstarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
let updateableStarredArticleIDs = receivedStarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
Task { @MainActor in
var errorOccurred = false
var errorOccurred = false
do {
try await account?.markAsUnread(updateableUnreadArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing unread statuses: %@", error.localizedDescription)
}
do {
try await account?.markAsRead(updateableReadArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing read statuses: %@", error.localizedDescription)
}
do {
try await account?.markAsUnstarred(updateableUnstarredArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing unstarred statuses: %@", error.localizedDescription)
}
do {
try await account?.markAsStarred(updateableStarredArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing starred statuses: %@", error.localizedDescription)
}
let parsedItems = await Self.makeParsedItems(records)
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
for (feedID, parsedItems) in feedIDsAndItems {
do {
try await account?.markAsUnread(updateableUnreadArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing unread statuses: %@", error.localizedDescription)
}
do {
try await account?.markAsRead(updateableReadArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing read statuses: %@", error.localizedDescription)
}
do {
try await account?.markAsUnstarred(updateableUnstarredArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing unstarred statuses: %@", error.localizedDescription)
}
do {
try await account?.markAsStarred(updateableStarredArticleIDs)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing starred statuses: %@", error.localizedDescription)
}
let group = DispatchGroup()
group.enter()
compressionQueue.async {
let parsedItems = records.compactMap { Self.makeParsedItem($0) }
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
Task { @MainActor in
for (feedID, parsedItems) in feedIDsAndItems {
group.enter()
do {
let articleChanges = try await self.account?.update(feedID: feedID, with: parsedItems, deleteOlder: false)
guard let deletes = articleChanges?.deletedArticles, !deletes.isEmpty else {
group.leave()
return
}
let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) }
try? await self.database.insertStatuses(syncStatuses)
group.leave()
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing articles: %@", error.localizedDescription)
group.leave()
}
}
group.leave()
let articleChanges = try await self.account?.update(feedID: feedID, with: parsedItems, deleteOlder: false)
guard let deletes = articleChanges?.deletedArticles, !deletes.isEmpty else {
continue
}
}
group.notify(queue: DispatchQueue.main) {
if errorOccurred {
completion(.failure(CloudKitZoneError.unknown))
} else {
completion(.success(()))
}
let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) }
try? await self.database.insertStatuses(syncStatuses)
} catch {
errorOccurred = true
os_log(.error, log: self.log, "Error occurred while storing articles: %@", error.localizedDescription)
}
}
if errorOccurred {
throw CloudKitZoneError.unknown
}
}
func stripPrefix(_ externalID: String) -> String {
return String(externalID[externalID.index(externalID.startIndex, offsetBy: 2)..<externalID.endIndex])
}
private static func makeParsedItems(_ articleRecords: [CKRecord]) async -> Set<ParsedItem> {
let task = Task.detached { () -> Set<ParsedItem> in
let parsedItems = articleRecords.compactMap { makeParsedItem($0) }
return Set(parsedItems)
}
return await task.value
}
static func makeParsedItem(_ articleRecord: CKRecord) -> ParsedItem? {
guard articleRecord.recordType == CloudKitArticlesZone.CloudKitArticle.recordType else {
return nil

View File

@ -445,32 +445,59 @@ private extension FeedbinAccountDelegate {
}
}
private func delay(seconds: Double) async {
await withCheckedContinuation { continuation in
self.performBlockAfter(seconds: seconds) {
continuation.resume()
}
}
}
nonisolated private func performBlockAfter(seconds: Double, block: @escaping @Sendable @MainActor () -> ()) {
let delayTime = DispatchTime.now() + seconds
DispatchQueue.main.asyncAfter(deadline: delayTime) {
Task { @MainActor in
block()
}
}
}
func checkImportResult(opmlImportResultID: Int, completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
DispatchQueue.main.async {
Task { @MainActor in
Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { timer in
var retry = 0
let maxRetries = 6 // a guess at a good number
Task { @MainActor in
@MainActor func checkResult() async {
os_log(.debug, log: self.log, "Checking status of OPML import...")
if retry >= maxRetries {
return
}
retry = retry + 1
do {
let importResult = try await self.caller.retrieveOPMLImportResult(importID: opmlImportResultID)
await delay(seconds: 15)
os_log(.debug, log: self.log, "Checking status of OPML import...")
if let importResult, importResult.complete {
os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.")
timer.invalidate()
completion(.success(()))
}
do {
let importResult = try await self.caller.retrieveOPMLImportResult(importID: opmlImportResultID)
} catch {
os_log(.debug, log: self.log, "Import OPML check failed.")
timer.invalidate()
completion(.failure(error))
if let importResult, importResult.complete {
os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.")
completion(.success(()))
} else {
await checkResult()
}
} catch {
os_log(.debug, log: self.log, "Import OPML check failed.")
completion(.failure(error))
}
}
await checkResult()
}
}

View File

@ -15,10 +15,7 @@ import AppKit
public final class UserApp {
public let bundleID: String
public var icon: NSImage? = nil
public var existsOnDisk = false
public var path: String? = nil
public var runningApplication: NSRunningApplication? = nil
public var isRunning: Bool {
@ -29,6 +26,10 @@ public final class UserApp {
return false
}
private var icon: NSImage? = nil
private var path: String? = nil
private var runningApplication: NSRunningApplication? = nil
public init(bundleID: String) {
self.bundleID = bundleID

View File

@ -29,7 +29,7 @@ public protocol SendToCommand {
/// The image for the command.
///
/// Often the icon of the target application.
var image: RSImage? { get }
@MainActor var image: RSImage? { get }
/// Determine whether an object can be sent to the target application.
///
@ -37,7 +37,7 @@ public protocol SendToCommand {
/// - object: The object to test.
/// - selectedText: The currently selected text.
/// - Returns: `true` if the object can be sent, `false` otherwise.
func canSendObject(_ object: Any?, selectedText: String?) -> Bool
@MainActor func canSendObject(_ object: Any?, selectedText: String?) -> Bool
/// Send an object to the target application.
///

View File

@ -20,7 +20,7 @@ public extension Notification.Name {
public protocol FeedIconDownloaderDelegate: Sendable {
var appIconImage: IconImage? { get }
@MainActor var appIconImage: IconImage? { get }
func downloadMetadata(_ url: String) async throws -> RSHTMLMetadata?
}

View File

@ -11,14 +11,14 @@ import WebKit
import Articles
final class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
var currentArticle: Article?
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
Task { @MainActor in
MainActor.assumeIsolated {
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
guard let responseURL = urlSchemeTask.request.url, let iconImage = self.currentArticle?.iconImage() else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}

View File

@ -780,7 +780,7 @@ extension MainWindowController: NSToolbarDelegate {
case .sidebarToggle:
let title = NSLocalizedString("Toggle Sidebar", comment: "Toggle Sidebar")
return buildToolbarButton(.toggleSidebar, title, AppAssets.sidebarToggleImage, "toggleTheSidebar:")
return buildToolbarButton(.sidebarToggle, title, AppAssets.sidebarToggleImage, "toggleTheSidebar:")
case .refresh:
let title = NSLocalizedString("Refresh", comment: "Refresh")

View File

@ -18,15 +18,18 @@ import Core
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
MainActor.assumeIsolated {
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
}
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? {
return sharingServiceDelegate
}
static func customSharingServices(for items: [Any]) -> [NSSharingService] {
@MainActor static func customSharingServices(for items: [Any]) -> [NSSharingService] {
let customServices: [SendToCommand] = [SendToMarsEditCommand(), SendToMicroBlogCommand()]
return customServices.compactMap { (sendToCommand) -> NSSharingService? in

View File

@ -7,8 +7,9 @@
//
import SafariServices
import os
class SafariExtensionHandler: SFSafariExtensionHandler {
final class SafariExtensionHandler: SFSafariExtensionHandler {
// Safari App Extensions don't support any reasonable means of detecting whether a
// specific Safari page was loaded with the benefit of the extension's injected
@ -19,8 +20,9 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
// I tried to use a NSMapTable from String to the closure directly, but Swift
// complains that the object has to be a class type.
typealias ValidationHandler = (Bool, String) -> Void
class ValidationWrapper {
typealias ValidationHandler = @Sendable (Bool, String) -> Void
final class ValidationWrapper: Sendable {
let validationHandler: ValidationHandler
init(validationHandler: @escaping ValidationHandler) {
@ -29,7 +31,16 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
}
// Maps from UUID to a validation wrapper
static var gPingPongMap = Dictionary<String, ValidationWrapper>()
private static let _gPingPongMap: OSAllocatedUnfairLock<Dictionary<String, ValidationWrapper>> = OSAllocatedUnfairLock(initialState: [String: ValidationWrapper]())
static var gPingPongMap: [String: ValidationWrapper] {
get {
_gPingPongMap.withLock { $0 }
}
set {
_gPingPongMap.withLock { $0 = newValue }
}
}
static let validationQueue = DispatchQueue(label: "Toolbar Validation")
// Bottleneck for calling through to a validation handler we have saved, and removing it from the list.
@ -79,7 +90,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
}
}
override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {
override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping (Bool, String) -> Void) {
let uniqueValidationID = NSUUID().uuidString

View File

@ -9,7 +9,8 @@
import Foundation
protocol ScriptingObject {
var objectSpecifier: NSScriptObjectSpecifier? { get }
@MainActor var objectSpecifier: NSScriptObjectSpecifier? { get }
@MainActor var scriptingKey: String { get }
}

View File

@ -15,7 +15,7 @@ final class ShareViewController: NSViewController {
@IBOutlet weak var nameTextField: NSTextField!
@IBOutlet weak var folderPopUpButton: NSPopUpButton!
private var url: URL?
nonisolated(unsafe) private var url: URL?
private var extensionContainers: ExtensionContainers?
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ShareViewController")

View File

@ -18,6 +18,6 @@ extension AppDelegate: FaviconDownloaderDelegate, FeedIconDownloaderDelegate {
func downloadMetadata(_ url: String) async throws -> RSHTMLMetadata? {
try await HTMLMetadataDownloader.downloadMetadata(for: url)
await HTMLMetadataDownloader.downloadMetadata(for: url)
}
}

View File

@ -9,9 +9,9 @@
import AppKit
import Articles
import Core
import AppKitExtras
@preconcurrency import AppKitExtras
final class SendToMarsEditCommand: SendToCommand {
@MainActor final class SendToMarsEditCommand: SendToCommand {
let title = "MarsEdit"
let image: RSImage? = AppAssets.marsEditIcon
@ -44,9 +44,9 @@ final class SendToMarsEditCommand: SendToCommand {
}
}
private extension SendToMarsEditCommand {
@MainActor private extension SendToMarsEditCommand {
@MainActor func send(_ article: Article, to app: UserApp) {
func send(_ article: Article, to app: UserApp) {
// App has already been launched.

View File

@ -9,11 +9,11 @@
import AppKit
import Articles
import Core
import AppKitExtras
@preconcurrency import AppKitExtras
// Not undoable.
final class SendToMicroBlogCommand: SendToCommand {
@MainActor final class SendToMicroBlogCommand: SendToCommand {
let title = "Micro.blog"
let image: RSImage? = AppAssets.microblogIcon
@ -30,7 +30,7 @@ final class SendToMicroBlogCommand: SendToCommand {
return true
}
@MainActor func sendObject(_ object: Any?, selectedText: String?) {
func sendObject(_ object: Any?, selectedText: String?) {
guard canSendObject(object, selectedText: selectedText) else {
return

View File

@ -12,9 +12,9 @@ import Web
struct CacheCleaner {
static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CacheCleaner")
nonisolated(unsafe) static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CacheCleaner")
static func purgeIfNecessary() {
@MainActor static func purgeIfNecessary() {
guard let flushDate = AppDefaults.shared.lastImageCacheFlushDate else {
AppDefaults.shared.lastImageCacheFlushDate = Date()

View File

@ -12,7 +12,7 @@ import Account
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter, Sendable {
private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
nonisolated(unsafe) private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
private static let filePath: String = {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String

View File

@ -10,7 +10,7 @@ import Foundation
struct ShareDefaultContainer {
static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? {
@MainActor static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? {
if let accountID = AppDefaults.shared.addFeedAccountID, let account = containers.accounts.first(where: { $0.accountID == accountID }) {
if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.folders.first(where: { $0.name == folderName }) {
@ -26,7 +26,7 @@ struct ShareDefaultContainer {
}
static func saveDefaultContainer(_ container: ExtensionContainer) {
@MainActor static func saveDefaultContainer(_ container: ExtensionContainer) {
AppDefaults.shared.addFeedAccountID = container.accountID
if let folder = container as? ExtensionFolder {
AppDefaults.shared.addFeedFolderName = folder.name

View File

@ -50,7 +50,7 @@ extension Array where Element == Article {
})
}
func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray {
@MainActor func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray {
return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed)
}

View File

@ -11,7 +11,7 @@ import Foundation
protocol SortableArticle {
var sortableName: String { get }
@MainActor var sortableName: String { get }
var sortableDate: Date { get }
var sortableArticleID: String { get }
var sortableFeedID: String { get }
@ -19,7 +19,7 @@ protocol SortableArticle {
struct ArticleSorter {
static func sortedByDate<T: SortableArticle>(articles: [T],
@MainActor static func sortedByDate<T: SortableArticle>(articles: [T],
sortDirection: ComparisonResult,
groupByFeed: Bool) -> [T] {
if groupByFeed {
@ -31,7 +31,7 @@ struct ArticleSorter {
// MARK: -
private static func sortedByFeedName<T: SortableArticle>(articles: [T],
@MainActor private static func sortedByFeedName<T: SortableArticle>(articles: [T],
sortByDateDirection: ComparisonResult) -> [T] {
// Group articles by "feed-feedID" - feed ID is used to differentiate between
// two feeds that have the same name

View File

@ -24,7 +24,7 @@ public final class WidgetDataEncoder {
private lazy var containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json")
static let shared = WidgetDataEncoder()
@MainActor static let shared = WidgetDataEncoder()
private init () {}
func encodeWidgetData() throws {

View File

@ -11,10 +11,11 @@ import Account
import Core
protocol AddFeedFolderViewControllerDelegate {
func didSelect(container: Container)
@MainActor func didSelect(container: Container)
}
class AddFeedFolderViewController: UITableViewController {
final class AddFeedFolderViewController: UITableViewController {
var delegate: AddFeedFolderViewControllerDelegate?
var addFeedType = AddFeedType.web

View File

@ -17,7 +17,7 @@ enum AddFeedType {
case web
}
class AddFeedViewController: UITableViewController {
final class AddFeedViewController: UITableViewController {
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var addButton: UIBarButtonItem!
@ -166,6 +166,7 @@ class AddFeedViewController: UITableViewController {
// MARK: AddFeedFolderViewControllerDelegate
extension AddFeedViewController: AddFeedFolderViewControllerDelegate {
func didSelect(container: Container) {
self.container = container
updateFolderLabel()

View File

@ -31,7 +31,7 @@ final class AppDefaults {
static let defaultThemeName = "Defaults"
static let shared = AppDefaults()
nonisolated(unsafe) static let shared = AppDefaults()
private init() {}
static let store: UserDefaults = {

View File

@ -37,7 +37,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
}
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
nonisolated(unsafe) let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
var userNotificationManager: UserNotificationManager!
var faviconDownloader: FaviconDownloader!
@ -204,11 +204,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.list, .banner, .badge, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
defer { completionHandler() }
let userInfo = response.notification.request.content.userInfo
@ -390,7 +391,9 @@ private extension AppDelegate {
do {
try BGTaskScheduler.shared.submit(request)
} catch {
os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription)
Task { @MainActor in
os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription)
}
}
}
}

View File

@ -8,7 +8,7 @@
import Foundation
import WebKit
import Images
@preconcurrency import Images
protocol ArticleIconSchemeHandlerDelegate: AnyObject {

View File

@ -8,11 +8,11 @@
import UIKit
@objc protocol SearchBarDelegate: NSObjectProtocol {
@objc optional func nextWasPressed(_ searchBar: ArticleSearchBar)
@objc optional func previousWasPressed(_ searchBar: ArticleSearchBar)
@objc optional func doneWasPressed(_ searchBar: ArticleSearchBar)
@objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
protocol SearchBarDelegate: AnyObject {
@MainActor func nextWasPressed(_ searchBar: ArticleSearchBar)
@MainActor func previousWasPressed(_ searchBar: ArticleSearchBar)
@MainActor func doneWasPressed(_ searchBar: ArticleSearchBar)
@MainActor func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
}
@ -146,7 +146,7 @@ private extension ArticleSearchBar {
private extension ArticleSearchBar {
@objc func textDidChange(_ notification: Notification) {
delegate?.searchBar?(self, textDidChange: searchField.text ?? "")
delegate?.searchBar(self, textDidChange: searchField.text ?? "")
if searchField.text?.isEmpty ?? true {
searchField.rightViewMode = .never
@ -156,21 +156,21 @@ private extension ArticleSearchBar {
}
@objc func nextPressed() {
delegate?.nextWasPressed?(self)
delegate?.nextWasPressed(self)
}
@objc func previousPressed() {
delegate?.previousWasPressed?(self)
delegate?.previousWasPressed(self)
}
@objc func donePressed(_ _: Any? = nil) {
delegate?.doneWasPressed?(self)
delegate?.doneWasPressed(self)
}
}
extension ArticleSearchBar: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
delegate?.nextWasPressed?(self)
delegate?.nextWasPressed(self)
return false
}
}

View File

@ -9,8 +9,9 @@
import UIKit
@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate {
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView)
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView)
@MainActor func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView)
@MainActor func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView)
}
open class ImageScrollView: UIScrollView {

View File

@ -42,8 +42,9 @@ class OpenInBrowserActivity: UIActivity {
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
Task { @MainActor in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
activityDidFinish(true)
}
}

View File

@ -17,10 +17,11 @@ import ArticleExtractor
import Images
protocol WebViewControllerDelegate: AnyObject {
func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
@MainActor func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
}
class WebViewController: UIViewController {
final class WebViewController: UIViewController {
private struct MessageName {
static let imageWasClicked = "imageWasClicked"

View File

@ -11,9 +11,10 @@ import os.log
struct ErrorHandler {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
nonisolated(unsafe) private static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
public static func present(_ viewController: UIViewController) -> (Error) -> () {
public static func present(_ viewController: UIViewController) -> @MainActor (Error) -> () {
return { @MainActor [weak viewController] error in
if UIApplication.shared.applicationState == .active {
viewController?.presentError(error)

View File

@ -12,7 +12,8 @@ import Tree
import Images
protocol FeedTableViewCellDelegate: AnyObject {
func feedTableViewCellDisclosureDidToggle(_ sender: FeedTableViewCell, expanding: Bool)
@MainActor func feedTableViewCellDisclosureDidToggle(_ sender: FeedTableViewCell, expanding: Bool)
}
class FeedTableViewCell : VibrantTableViewCell {

View File

@ -10,7 +10,8 @@ import UIKit
import UIKitExtras
protocol FeedTableViewSectionHeaderDelegate {
func FeedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader)
@MainActor func feedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader)
}
class FeedTableViewSectionHeader: UITableViewHeaderFooterView {
@ -139,7 +140,7 @@ class FeedTableViewSectionHeader: UITableViewHeaderFooterView {
private extension FeedTableViewSectionHeader {
@objc func toggleDisclosure() {
delegate?.FeedTableViewSectionHeaderDisclosureDidToggle(self)
delegate?.feedTableViewSectionHeaderDisclosureDidToggle(self)
}
func commonInit() {

View File

@ -669,7 +669,7 @@ extension SidebarViewController: UIContextMenuInteractionDelegate {
extension SidebarViewController: FeedTableViewSectionHeaderDelegate {
func FeedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader) {
func feedTableViewSectionHeaderDisclosureDidToggle(_ sender: FeedTableViewSectionHeader) {
toggle(sender)
}

View File

@ -11,7 +11,8 @@ import UIKit
import Core
protocol AddAccountDismissDelegate: UIViewController {
func dismiss()
@MainActor func dismiss()
}
class AddAccountViewController: UITableViewController, AddAccountDismissDelegate {

View File

@ -10,7 +10,8 @@ import UIKit
import Account
protocol ShareFolderPickerControllerDelegate: AnyObject {
func shareFolderPickerDidSelect(_ container: ExtensionContainer)
@MainActor func shareFolderPickerDidSelect(_ container: ExtensionContainer)
}
class ShareFolderPickerController: UITableViewController {

View File

@ -12,7 +12,7 @@ import UIKit
// Uses a cache.
// Main thready only.
final class SingleLineUILabelSizer {
@MainActor final class SingleLineUILabelSizer {
let font: UIFont
private var cache = [String: CGSize]()
@ -36,7 +36,7 @@ final class SingleLineUILabelSizer {
}
static private var sizers = [UIFont: SingleLineUILabelSizer]()
@MainActor static private var sizers = [UIFont: SingleLineUILabelSizer]()
static func sizer(for font: UIFont) -> SingleLineUILabelSizer {

View File

@ -74,7 +74,7 @@ struct TimelineAccessibilityCellLayout: TimelineCellLayout {
private extension TimelineAccessibilityCellLayout {
static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
@MainActor static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero

View File

@ -94,7 +94,7 @@ extension TimelineCellLayout {
}
static func rectForFeedName(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
@MainActor static func rectForFeedName(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero
r.origin = point

View File

@ -109,7 +109,7 @@ struct TimelineDefaultCellLayout: TimelineCellLayout {
extension TimelineDefaultCellLayout {
static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
@MainActor static func rectForDate(_ cellData: TimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero

View File

@ -8,7 +8,7 @@
import UIKit
class TimelineTitleView: UIView {
final class TimelineTitleView: UIView {
@IBOutlet weak var iconView: IconView!
@IBOutlet weak var label: UILabel!

View File

@ -44,7 +44,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 17.0
//SDKROOT = macosx
SWIFT_SWIFT3_OBJC_INFERENCE = Off
SWIFT_VERSION = 5.10
SWIFT_STRICT_CONCURRENCY = complete
SWIFT_STRICT_CONCURRENCY = targeted
ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO
// https://forums.swift.org/t/swift-packages-in-multiple-targets-results-in-this-will-result-in-duplication-of-library-code-errors/34892/33

View File

@ -8,3 +8,4 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
COMBINE_HIDPI_IMAGES = YES
MACOSX_DEPLOYMENT_TARGET = 14.0
SDKROOT = macosx;
SWIFT_STRICT_CONCURRENCY = complete