From d1209e67044e7bb262389296ce24512d95eae58b Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Sun, 26 Feb 2023 21:39:07 -0800 Subject: [PATCH] Updated to resolve all possible Sendability warnings from Swift 6 compatibility mode. (#1072) Co-authored-by: Jim Dovey --- .../ActionRequestHandler.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 9 +++ .../NotificationService.swift | 5 ++ .../Sources/Account/AccountDetailView.swift | 3 + .../AccountsList/AccountsListRow.swift | 1 + .../Sources/Account/Follow/FollowButton.swift | 1 + .../AppAccount/AppAccountViewModel.swift | 6 ++ .../AppAccount/AppAccountsManager.swift | 1 + .../Sources/DesignSystem/SceneDelegate.swift | 1 + Packages/Env/Sources/Env/CurrentAccount.swift | 1 + .../Env/Sources/Env/CurrentInstance.swift | 1 + .../Env/PushNotificationsService.swift | 1 + Packages/Env/Sources/Env/QuickLook.swift | 7 ++ Packages/Env/Sources/Env/Router.swift | 5 +- Packages/Env/Sources/Env/StreamWatcher.swift | 1 + .../Env/Sources/Env/UserPreferences.swift | 11 ++++ .../Lists/Edit/ListEditViewModel.swift | 1 + Packages/Models/Package.swift | 6 +- Packages/Models/Sources/Models/Account.swift | 2 + .../Models/Sources/Models/AppAccount.swift | 2 + Packages/Models/Sources/Models/Card.swift | 2 + .../Models/ConsolidatedNotification.swift | 2 + .../Models/Sources/Models/Conversation.swift | 2 + Packages/Models/Sources/Models/Filter.swift | 5 ++ .../Models/Sources/Models/InstanceApp.swift | 2 + Packages/Models/Sources/Models/Language.swift | 2 + Packages/Models/Sources/Models/List.swift | 2 + .../Models/MastodonPushNotification.swift | 2 + .../Sources/Models/MediaAttachement.swift | 5 ++ Packages/Models/Sources/Models/Mention.swift | 2 + .../Models/Sources/Models/Notification.swift | 3 + Packages/Models/Sources/Models/Poll.swift | 4 ++ .../Sources/Models/PushSubscription.swift | 3 + .../Models/Sources/Models/Relationship.swift | 2 + .../Models/Sources/Models/SearchResults.swift | 2 + .../Models/Sources/Models/ServerError.swift | 2 + .../Sources/Models/ServerPreferences.swift | 3 + Packages/Models/Sources/Models/Status.swift | 15 ++++- .../Models/Sources/Models/StatusContext.swift | 2 + .../Models/Sources/Models/StatusHistory.swift | 2 + .../Sources/Models/StatusTranslation.swift | 2 + Packages/Models/Sources/Models/Tag.swift | 4 ++ Packages/Network/Sources/Network/Client.swift | 66 ++++++++++++------- .../Network/Sources/Network/LinkHandler.swift | 2 + .../Sources/Network/OpenAIClient.swift | 6 ++ .../StatusEditorUTTypeSupported.swift | 13 ++++ .../Status/Editor/StatusEditorViewModel.swift | 1 + .../Sources/Status/List/StatusesFetcher.swift | 1 + .../Status/Poll/StatusPollViewModel.swift | 1 + .../Status/Row/StatusRowViewModel.swift | 1 + .../Row/Subviews/StatusRowActionsView.swift | 10 +++ .../Sources/Timeline/TimelineCache.swift | 6 ++ 52 files changed, 216 insertions(+), 28 deletions(-) diff --git a/IceCubesActionExtension/ActionRequestHandler.swift b/IceCubesActionExtension/ActionRequestHandler.swift index 6e7aa21b..13c24963 100644 --- a/IceCubesActionExtension/ActionRequestHandler.swift +++ b/IceCubesActionExtension/ActionRequestHandler.swift @@ -15,7 +15,7 @@ import Network // Sample code was sending this from a thread to another, let asume @Sendable for this extension NSExtensionContext: @unchecked Sendable {} -class ActionRequestHandler: NSObject, NSExtensionRequestHandling { +final class ActionRequestHandler: NSObject, NSExtensionRequestHandling, Sendable { enum Error: Swift.Error { case inputProviderNotFound case loadedItemHasWrongType diff --git a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e5f424ce..e6a8b3e1 100644 --- a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -63,6 +63,15 @@ "version" : "0.13.3" } }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "ff3d2212b6b093db7f177d0855adbc4ef9c5f036", + "version" : "1.0.3" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", diff --git a/IceCubesNotifications/NotificationService.swift b/IceCubesNotifications/NotificationService.swift index f484a291..8878db9c 100644 --- a/IceCubesNotifications/NotificationService.swift +++ b/IceCubesNotifications/NotificationService.swift @@ -82,6 +82,11 @@ class NotificationService: UNNotificationServiceExtension { let fileURL = temporaryDirectoryURL.appendingPathComponent(filename) Task { + // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated + // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor + // boundary. + // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`. + // There is a Radar tracking this & others like it. if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) { if let image = UIImage(data: data) { try? image.pngData()?.write(to: fileURL) diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index af4c4ca3..b4c68b2f 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -82,6 +82,9 @@ public struct AccountDetailView: View { isCurrentUser = currentAccount.account?.id == viewModel.accountId viewModel.isCurrentUser = isCurrentUser viewModel.client = client + + // Avoid capturing non-Sendable `self` just to access the view model. + let viewModel = self.viewModel Task { await withTaskGroup(of: Void.self) { group in group.addTask { await viewModel.fetchAccount() } diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift index 1f5bce97..87f7c86d 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift @@ -1,3 +1,4 @@ +import Combine import DesignSystem import EmojiText import Env diff --git a/Packages/Account/Sources/Account/Follow/FollowButton.swift b/Packages/Account/Sources/Account/Follow/FollowButton.swift index 387ead79..9da0e249 100644 --- a/Packages/Account/Sources/Account/Follow/FollowButton.swift +++ b/Packages/Account/Sources/Account/Follow/FollowButton.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Models import Network diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift index be134894..84a5d152 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift @@ -1,3 +1,4 @@ +import Combine import DesignSystem import Models import Network @@ -61,6 +62,11 @@ public class AppAccountViewModel: ObservableObject { } private func refreshAvatar(account: Account) async { + // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated + // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor + // boundary. + // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`. + // There is a Radar tracking this & others like it. if let (data, _) = try? await URLSession.shared.data(from: account.avatar), let image = UIImage(data: data)?.roundedImage { diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift index 6efb6e8a..2eaa3103 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift @@ -1,3 +1,4 @@ +import Combine import Env import Models import Network diff --git a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift index fea179f7..bd5d84c4 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift @@ -1,3 +1,4 @@ +import Combine import UIKit public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { diff --git a/Packages/Env/Sources/Env/CurrentAccount.swift b/Packages/Env/Sources/Env/CurrentAccount.swift index 5477800b..7ab1abc9 100644 --- a/Packages/Env/Sources/Env/CurrentAccount.swift +++ b/Packages/Env/Sources/Env/CurrentAccount.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Models import Network diff --git a/Packages/Env/Sources/Env/CurrentInstance.swift b/Packages/Env/Sources/Env/CurrentInstance.swift index 558f6346..beec3dfb 100644 --- a/Packages/Env/Sources/Env/CurrentInstance.swift +++ b/Packages/Env/Sources/Env/CurrentInstance.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Models import Network diff --git a/Packages/Env/Sources/Env/PushNotificationsService.swift b/Packages/Env/Sources/Env/PushNotificationsService.swift index 091e0804..7188fe3c 100644 --- a/Packages/Env/Sources/Env/PushNotificationsService.swift +++ b/Packages/Env/Sources/Env/PushNotificationsService.swift @@ -1,3 +1,4 @@ +import Combine import CryptoKit import Foundation import KeychainSwift diff --git a/Packages/Env/Sources/Env/QuickLook.swift b/Packages/Env/Sources/Env/QuickLook.swift index d067e774..89fb8d2b 100644 --- a/Packages/Env/Sources/Env/QuickLook.swift +++ b/Packages/Env/Sources/Env/QuickLook.swift @@ -1,3 +1,4 @@ +import Combine import QuickLook import SwiftUI @@ -69,6 +70,12 @@ public class QuickLook: ObservableObject { private func localPathFor(url: URL) async throws -> URL { try? FileManager.default.createDirectory(at: quickLookDir, withIntermediateDirectories: true) let path = quickLookDir.appendingPathComponent(url.lastPathComponent) + + // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated + // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor + // boundary. + // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`. + // There is a Radar tracking this & others like it. let data = try await URLSession.shared.data(from: url).0 try data.write(to: path) return path diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 7e9e1009..354ec4d2 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Models import Network @@ -140,7 +141,7 @@ public class RouterPath: ObservableObject { if let account = results?.accounts.first { navigate(to: .accountDetailWithAccount(account: account)) } else { - await UIApplication.shared.open(url) + _ = await UIApplication.shared.open(url) } } @@ -154,7 +155,7 @@ public class RouterPath: ObservableObject { if let account = results?.accounts.first { navigate(to: .accountDetailWithAccount(account: account)) } else { - await UIApplication.shared.open(url) + _ = await UIApplication.shared.open(url) } } } diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index ff7714d6..402b52fe 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Models import Network diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 887c3739..85e35697 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import Models import Network @@ -61,6 +62,16 @@ public class UserPreferences: ObservableObject { return "enum.swipeactions.icon-only" } } + + // Have to implement this manually here due to compiler not implicitly + // inserting `nonisolated`, which leads to a warning: + // + // Main actor-isolated static property 'allCases' cannot be used to + // satisfy nonisolated protocol requirement + // + nonisolated public static var allCases: [Self] { + [.iconWithText, .iconOnly] + } } public var postVisibility: Models.Visibility { diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift index cac0b84c..84598cbc 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift @@ -1,3 +1,4 @@ +import Combine import Models import Network import SwiftUI diff --git a/Packages/Models/Package.swift b/Packages/Models/Package.swift index b7e4b5ab..f1a6a412 100644 --- a/Packages/Models/Package.swift +++ b/Packages/Models/Package.swift @@ -17,11 +17,15 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.3"), ], targets: [ .target( name: "Models", - dependencies: ["SwiftSoup"] + dependencies: [ + "SwiftSoup", + .product(name: "Atomics", package: "swift-atomics") + ] ), .testTarget( name: "ModelsTests", diff --git a/Packages/Models/Sources/Models/Account.swift b/Packages/Models/Sources/Models/Account.swift index f95b31e4..e511ac75 100644 --- a/Packages/Models/Sources/Models/Account.swift +++ b/Packages/Models/Sources/Models/Account.swift @@ -121,3 +121,5 @@ public struct FamiliarAccounts: Decodable { public let id: String public let accounts: [Account] } + +extension FamiliarAccounts: Sendable {} diff --git a/Packages/Models/Sources/Models/AppAccount.swift b/Packages/Models/Sources/Models/AppAccount.swift index 77070673..4521400a 100644 --- a/Packages/Models/Sources/Models/AppAccount.swift +++ b/Packages/Models/Sources/Models/AppAccount.swift @@ -27,3 +27,5 @@ public struct AppAccount: Codable, Identifiable, Hashable { self.oauthToken = oauthToken } } + +extension AppAccount: Sendable {} diff --git a/Packages/Models/Sources/Models/Card.swift b/Packages/Models/Sources/Models/Card.swift index a7cadc1f..132ead30 100644 --- a/Packages/Models/Sources/Models/Card.swift +++ b/Packages/Models/Sources/Models/Card.swift @@ -11,3 +11,5 @@ public struct Card: Codable, Identifiable, Equatable, Hashable { public let type: String public let image: URL? } + +extension Card: Sendable {} diff --git a/Packages/Models/Sources/Models/ConsolidatedNotification.swift b/Packages/Models/Sources/Models/ConsolidatedNotification.swift index acc55153..5716192d 100644 --- a/Packages/Models/Sources/Models/ConsolidatedNotification.swift +++ b/Packages/Models/Sources/Models/ConsolidatedNotification.swift @@ -41,3 +41,5 @@ public struct ConsolidatedNotification: Identifiable { [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] } } + +extension ConsolidatedNotification: Sendable {} diff --git a/Packages/Models/Sources/Models/Conversation.swift b/Packages/Models/Sources/Models/Conversation.swift index f6f7266e..7935cf88 100644 --- a/Packages/Models/Sources/Models/Conversation.swift +++ b/Packages/Models/Sources/Models/Conversation.swift @@ -22,3 +22,5 @@ public struct Conversation: Identifiable, Decodable, Hashable, Equatable { .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] } } + +extension Conversation: Sendable {} diff --git a/Packages/Models/Sources/Models/Filter.swift b/Packages/Models/Sources/Models/Filter.swift index e5b8acbc..4dd32f89 100644 --- a/Packages/Models/Sources/Models/Filter.swift +++ b/Packages/Models/Sources/Models/Filter.swift @@ -20,3 +20,8 @@ public struct Filter: Codable, Identifiable, Equatable, Hashable { public let context: [String] public let filterAction: Action } + +extension Filtered: Sendable {} +extension Filter: Sendable {} +extension Filter.Action: Sendable {} +extension Filter.Context: Sendable {} diff --git a/Packages/Models/Sources/Models/InstanceApp.swift b/Packages/Models/Sources/Models/InstanceApp.swift index e842ab7c..8f41ba9a 100644 --- a/Packages/Models/Sources/Models/InstanceApp.swift +++ b/Packages/Models/Sources/Models/InstanceApp.swift @@ -9,3 +9,5 @@ public struct InstanceApp: Codable, Identifiable { public let clientSecret: String public let vapidKey: String? } + +extension InstanceApp: Sendable {} diff --git a/Packages/Models/Sources/Models/Language.swift b/Packages/Models/Sources/Models/Language.swift index ea750190..ab009130 100644 --- a/Packages/Models/Sources/Models/Language.swift +++ b/Packages/Models/Sources/Models/Language.swift @@ -18,3 +18,5 @@ public struct Language: Identifiable, Equatable, Hashable { ) } } + +extension Language: Sendable {} diff --git a/Packages/Models/Sources/Models/List.swift b/Packages/Models/Sources/Models/List.swift index ef324fcb..052697b0 100644 --- a/Packages/Models/Sources/Models/List.swift +++ b/Packages/Models/Sources/Models/List.swift @@ -5,3 +5,5 @@ public struct List: Codable, Identifiable, Equatable, Hashable { public let title: String public let repliesPolicy: String } + +extension List: Sendable {} diff --git a/Packages/Models/Sources/Models/MastodonPushNotification.swift b/Packages/Models/Sources/Models/MastodonPushNotification.swift index 54c983b3..2d86ea08 100644 --- a/Packages/Models/Sources/Models/MastodonPushNotification.swift +++ b/Packages/Models/Sources/Models/MastodonPushNotification.swift @@ -21,3 +21,5 @@ public struct MastodonPushNotification: Codable { case body } } + +extension MastodonPushNotification: Sendable {} diff --git a/Packages/Models/Sources/Models/MediaAttachement.swift b/Packages/Models/Sources/Models/MediaAttachement.swift index 348645e6..b9065d26 100644 --- a/Packages/Models/Sources/Models/MediaAttachement.swift +++ b/Packages/Models/Sources/Models/MediaAttachement.swift @@ -29,3 +29,8 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable { public let description: String? public let meta: MetaContainer? } + +extension MediaAttachment: Sendable {} +extension MediaAttachment.MetaContainer: Sendable {} +extension MediaAttachment.MetaContainer.Meta: Sendable {} +extension MediaAttachment.SupportedType: Sendable {} diff --git a/Packages/Models/Sources/Models/Mention.swift b/Packages/Models/Sources/Models/Mention.swift index ca3c3a14..588b3d8d 100644 --- a/Packages/Models/Sources/Models/Mention.swift +++ b/Packages/Models/Sources/Models/Mention.swift @@ -6,3 +6,5 @@ public struct Mention: Codable, Equatable, Hashable { public let url: URL public let acct: String } + +extension Mention: Sendable {} diff --git a/Packages/Models/Sources/Models/Notification.swift b/Packages/Models/Sources/Models/Notification.swift index 690b572d..405a2de5 100644 --- a/Packages/Models/Sources/Models/Notification.swift +++ b/Packages/Models/Sources/Models/Notification.swift @@ -23,3 +23,6 @@ public struct Notification: Decodable, Identifiable, Equatable { status: .placeholder()) } } + +extension Notification: Sendable {} +extension Notification.NotificationType: Sendable {} diff --git a/Packages/Models/Sources/Models/Poll.swift b/Packages/Models/Sources/Models/Poll.swift index 31f6cd7e..4a159685 100644 --- a/Packages/Models/Sources/Models/Poll.swift +++ b/Packages/Models/Sources/Models/Poll.swift @@ -48,3 +48,7 @@ public struct NullableString: Codable, Equatable, Hashable { } } } + +extension Poll: Sendable {} +extension Poll.Option: Sendable {} +extension NullableString: Sendable {} diff --git a/Packages/Models/Sources/Models/PushSubscription.swift b/Packages/Models/Sources/Models/PushSubscription.swift index ecd3999c..d17075b2 100644 --- a/Packages/Models/Sources/Models/PushSubscription.swift +++ b/Packages/Models/Sources/Models/PushSubscription.swift @@ -15,3 +15,6 @@ public struct PushSubscription: Identifiable, Decodable { public let serverKey: String public let alerts: Alerts } + +extension PushSubscription: Sendable {} +extension PushSubscription.Alerts: Sendable {} diff --git a/Packages/Models/Sources/Models/Relationship.swift b/Packages/Models/Sources/Models/Relationship.swift index e1c8aa58..7dbf8f60 100644 --- a/Packages/Models/Sources/Models/Relationship.swift +++ b/Packages/Models/Sources/Models/Relationship.swift @@ -50,3 +50,5 @@ public extension Relationship { notifying = try values.decodeIfPresent(Bool.self, forKey: .notifying) ?? false } } + +extension Relationship: Sendable {} diff --git a/Packages/Models/Sources/Models/SearchResults.swift b/Packages/Models/Sources/Models/SearchResults.swift index 94f9fd8c..6c8514c2 100644 --- a/Packages/Models/Sources/Models/SearchResults.swift +++ b/Packages/Models/Sources/Models/SearchResults.swift @@ -14,3 +14,5 @@ public struct SearchResults: Decodable { accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty } } + +extension SearchResults: Sendable {} diff --git a/Packages/Models/Sources/Models/ServerError.swift b/Packages/Models/Sources/Models/ServerError.swift index 0a4db626..5b51414a 100644 --- a/Packages/Models/Sources/Models/ServerError.swift +++ b/Packages/Models/Sources/Models/ServerError.swift @@ -3,3 +3,5 @@ import Foundation public struct ServerError: Decodable, Error { public let error: String? } + +extension ServerError: Sendable {} diff --git a/Packages/Models/Sources/Models/ServerPreferences.swift b/Packages/Models/Sources/Models/ServerPreferences.swift index 53a7ff1f..c74b603d 100644 --- a/Packages/Models/Sources/Models/ServerPreferences.swift +++ b/Packages/Models/Sources/Models/ServerPreferences.swift @@ -33,3 +33,6 @@ public struct ServerPreferences: Decodable { case autoExpandSpoilers = "reading:expand:spoilers" } } + +extension ServerPreferences: Sendable {} +extension ServerPreferences.AutoExpandMedia: Sendable {} diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index 815c9c98..c43cc34e 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -1,4 +1,5 @@ import Foundation +import Atomics public struct Application: Codable, Identifiable, Hashable, Equatable { public var id: String { @@ -103,6 +104,7 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable public let sensitive: Bool public let language: String? + public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { self.id = id self.content = content @@ -228,7 +230,7 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha public let bookmarked: Bool? public let emojis: [Emoji] public let url: String? - public var application: Application? + public let application: Application? public let inReplyToId: String? public let inReplyToAccountId: String? public let visibility: Visibility @@ -267,3 +269,14 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha self.language = language } } + +extension Application: Sendable {} +extension StatusViewId: Sendable {} + +// Every property in Status is immutable. +extension Status: Sendable {} + +// Every property in ReblogStatus is immutable. +extension ReblogStatus: Sendable {} + + diff --git a/Packages/Models/Sources/Models/StatusContext.swift b/Packages/Models/Sources/Models/StatusContext.swift index 0fa4a6c0..63f81bde 100644 --- a/Packages/Models/Sources/Models/StatusContext.swift +++ b/Packages/Models/Sources/Models/StatusContext.swift @@ -8,3 +8,5 @@ public struct StatusContext: Decodable { .init(ancestors: [], descendants: []) } } + +extension StatusContext: Sendable {} diff --git a/Packages/Models/Sources/Models/StatusHistory.swift b/Packages/Models/Sources/Models/StatusHistory.swift index 5664c427..1d0f7fbd 100644 --- a/Packages/Models/Sources/Models/StatusHistory.swift +++ b/Packages/Models/Sources/Models/StatusHistory.swift @@ -9,3 +9,5 @@ public struct StatusHistory: Decodable, Identifiable { public let createdAt: ServerDate public let emojis: [Emoji] } + +extension StatusHistory: Sendable {} diff --git a/Packages/Models/Sources/Models/StatusTranslation.swift b/Packages/Models/Sources/Models/StatusTranslation.swift index 41939ba5..8b6390a8 100644 --- a/Packages/Models/Sources/Models/StatusTranslation.swift +++ b/Packages/Models/Sources/Models/StatusTranslation.swift @@ -11,3 +11,5 @@ public struct StatusTranslation: Decodable { self.provider = provider } } + +extension StatusTranslation: Sendable {} diff --git a/Packages/Models/Sources/Models/Tag.swift b/Packages/Models/Sources/Models/Tag.swift index 3db44e57..54b2841c 100644 --- a/Packages/Models/Sources/Models/Tag.swift +++ b/Packages/Models/Sources/Models/Tag.swift @@ -58,3 +58,7 @@ public struct FeaturedTag: Codable, Identifiable { } } } + +extension Tag: Sendable {} +extension Tag.History: Sendable {} +extension FeaturedTag: Sendable {} diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 2b79323f..2c54643c 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -1,12 +1,17 @@ +import Combine import Foundation import Models import SwiftUI +import os public final class Client: ObservableObject, Equatable, Identifiable, Hashable { public static func == (lhs: Client, rhs: Client) -> Bool { - lhs.isAuth == rhs.isAuth && + let lhsToken = lhs.critical.withLock { $0.oauthToken } + let rhsToken = rhs.critical.withLock { $0.oauthToken } + + return (lhsToken != nil) == (rhsToken != nil) && lhs.server == rhs.server && - lhs.oauthToken?.accessToken == rhs.oauthToken?.accessToken + lhsToken?.accessToken == rhsToken?.accessToken } public enum Version: String, Sendable { @@ -19,7 +24,10 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable { } public var id: String { - "\(isAuth)\(server)\(oauthToken?.createdAt ?? 0)" + critical.withLock { + let isAuth = $0.oauthToken != nil + return "\(isAuth)\(server)\($0.oauthToken?.createdAt ?? 0)" + } } public func hash(into hasher: inout Hasher) { @@ -28,42 +36,52 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable { public let server: String public let version: Version - public private(set) var connections: Set private let urlSession: URLSession private let decoder = JSONDecoder() - /// Only used as a transitionary app while in the oauth flow. - private var oauthApp: InstanceApp? - - private var oauthToken: OauthToken? + // Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client` + // provably `Sendable`. The lock is a struct, but it uses a `ManagedBuffer` + // reference type to hold its associated state. + private let critical: OSAllocatedUnfairLock + private struct Critical: Sendable { + /// Only used as a transitionary app while in the oauth flow. + var oauthApp: InstanceApp? + var oauthToken: OauthToken? + var connections: Set = [] + } public var isAuth: Bool { - oauthToken != nil + critical.withLock { $0.oauthToken != nil } + } + + public var connections: Set { + critical.withLock { $0.connections } } public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) { self.server = server self.version = version + self.critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server])) urlSession = URLSession.shared decoder.keyDecodingStrategy = .convertFromSnakeCase - self.oauthToken = oauthToken - connections = Set([server]) } public func addConnections(_ connections: [String]) { - connections.forEach { - self.connections.insert($0) + critical.withLock { + $0.connections.formUnion(connections) } } public func hasConnection(with url: URL) -> Bool { guard let host = url.host else { return false } - if let rootHost = host.split(separator: ".", maxSplits: 1).last { - // Sometimes the connection is with the root host instead of a subdomain - // eg. Mastodon runs on mastdon.domain.com but the connection is with domain.com - return connections.contains(host) || connections.contains(String(rootHost)) - } else { - return connections.contains(host) + return critical.withLock { + if let rootHost = host.split(separator: ".", maxSplits: 1).last { + // Sometimes the connection is with the root host instead of a subdomain + // eg. Mastodon runs on mastdon.domain.com but the connection is with domain.com + return $0.connections.contains(host) || $0.connections.contains(String(rootHost)) + } else { + return $0.connections.contains(host) + } } } @@ -87,7 +105,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable { private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest { var request = URLRequest(url: url) request.httpMethod = httpMethod - if let oauthToken { + if let oauthToken = critical.withLock({ $0.oauthToken }) { request.setValue("Bearer \(oauthToken.accessToken)", forHTTPHeaderField: "Authorization") } if let json = endpoint.jsonValue { @@ -175,12 +193,12 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable { public func oauthURL() async throws -> URL { let app: InstanceApp = try await post(endpoint: Apps.registerApp) - oauthApp = app + critical.withLock { $0.oauthApp = app } return makeURL(endpoint: Oauth.authorize(clientId: app.clientId)) } public func continueOauthFlow(url: URL) async throws -> OauthToken { - guard let app = oauthApp else { + guard let app = critical.withLock({ $0.oauthApp }) else { throw OauthError.missingApp } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), @@ -191,7 +209,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable { let token: OauthToken = try await post(endpoint: Oauth.token(code: code, clientId: app.clientId, clientSecret: app.clientSecret)) - oauthToken = token + critical.withLock { $0.oauthToken = token } return token } @@ -239,3 +257,5 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable { } } } + +extension Client: Sendable {} diff --git a/Packages/Network/Sources/Network/LinkHandler.swift b/Packages/Network/Sources/Network/LinkHandler.swift index ef6e5d06..10a3ffff 100644 --- a/Packages/Network/Sources/Network/LinkHandler.swift +++ b/Packages/Network/Sources/Network/LinkHandler.swift @@ -16,3 +16,5 @@ public struct LinkHandler { return nil } } + +extension LinkHandler: Sendable {} diff --git a/Packages/Network/Sources/Network/OpenAIClient.swift b/Packages/Network/Sources/Network/OpenAIClient.swift index f269ca06..b80937d0 100644 --- a/Packages/Network/Sources/Network/OpenAIClient.swift +++ b/Packages/Network/Sources/Network/OpenAIClient.swift @@ -105,3 +105,9 @@ public struct OpenAIClient { } } } + +extension OpenAIClient: Sendable {} +extension OpenAIClient.Prompt: Sendable {} +extension OpenAIClient.Request: Sendable {} +extension OpenAIClient.Response: Sendable {} +extension OpenAIClient.Response.Choice: Sendable {} diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift index 3fe7cef2..3776d627 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift @@ -24,6 +24,17 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { case uiimage = "com.apple.uikit.image" + // Have to implement this manually here due to compiler not implicitly + // inserting `nonisolated`, which leads to a warning: + // + // Main actor-isolated static property 'allCases' cannot be used to + // satisfy nonisolated protocol requirement + // + nonisolated public static var allCases: [StatusEditorUTTypeSupported] { + [.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video, + .movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage] + } + static func types() -> [UTType] { [.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie] } @@ -47,6 +58,8 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { } func loadItemContent(item: NSItemProvider) async throws -> Any? { + // Many warnings here about non-sendable type `[AnyHashable: Any]?` crossing + // actor boundaries. Many Radars have been filed. let result = try await item.loadItem(forTypeIdentifier: rawValue) if isVideo, let transferable = await getVideoTransferable(item: item) { return transferable diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 9355b880..d6de96bf 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -1,3 +1,4 @@ +import Combine import DesignSystem import Env import Models diff --git a/Packages/Status/Sources/Status/List/StatusesFetcher.swift b/Packages/Status/Sources/Status/List/StatusesFetcher.swift index 95a6f046..fdf4ad4e 100644 --- a/Packages/Status/Sources/Status/List/StatusesFetcher.swift +++ b/Packages/Status/Sources/Status/List/StatusesFetcher.swift @@ -1,3 +1,4 @@ +import Combine import Models import SwiftUI diff --git a/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift b/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift index 63acaf0a..ff22d07b 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift @@ -1,3 +1,4 @@ +import Combine import Models import Network import SwiftUI diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index f52650ca..286d70cd 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -1,3 +1,4 @@ +import Combine import Env import Models import NaturalLanguage diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift index 3485440b..f2d33f4b 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift @@ -17,6 +17,16 @@ struct StatusRowActionsView: View { enum Action: CaseIterable { case respond, boost, favorite, bookmark, share + // Have to implement this manually here due to compiler not implicitly + // inserting `nonisolated`, which leads to a warning: + // + // Main actor-isolated static property 'allCases' cannot be used to + // satisfy nonisolated protocol requirement + // + nonisolated public static var allCases: [StatusRowActionsView.Action] { + [.respond, .boost, .favorite, .bookmark, .share] + } + func iconName(viewModel: StatusRowViewModel, privateBoost: Bool = false) -> String { switch self { case .respond: diff --git a/Packages/Timeline/Sources/Timeline/TimelineCache.swift b/Packages/Timeline/Sources/Timeline/TimelineCache.swift index d8e394af..46ea3047 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineCache.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineCache.swift @@ -59,3 +59,9 @@ public actor TimelineCache { UserDefaults.standard.array(forKey: "timeline-last-seen-\(client.id)") as? [String] } } + +// Quiets down the warnings from this one. Bodega is nicely async so we don't +// want to just use `@preconcurrency`, but the CacheKey type is (incorrectly) +// not marked as `Sendable`---it's a value type containing two `String` +// properties. +extension Bodega.CacheKey: @unchecked Sendable {}