Updated to resolve all possible Sendability warnings from Swift 6 compatibility mode. (#1072)

Co-authored-by: Jim Dovey <jimdovey@apple.com>
This commit is contained in:
Jim Dovey 2023-02-26 21:39:07 -08:00 committed by GitHub
parent 9f026fbc42
commit d1209e6704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 216 additions and 28 deletions

View file

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

View file

@ -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",

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import Combine
import DesignSystem
import EmojiText
import Env

View file

@ -1,3 +1,4 @@
import Combine
import Foundation
import Models
import Network

View file

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

View file

@ -1,3 +1,4 @@
import Combine
import Env
import Models
import Network

View file

@ -1,3 +1,4 @@
import Combine
import UIKit
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {

View file

@ -1,3 +1,4 @@
import Combine
import Foundation
import Models
import Network

View file

@ -1,3 +1,4 @@
import Combine
import Foundation
import Models
import Network

View file

@ -1,3 +1,4 @@
import Combine
import CryptoKit
import Foundation
import KeychainSwift

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import Combine
import Foundation
import Models
import Network

View file

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

View file

@ -1,3 +1,4 @@
import Combine
import Models
import Network
import SwiftUI

View file

@ -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",

View file

@ -121,3 +121,5 @@ public struct FamiliarAccounts: Decodable {
public let id: String
public let accounts: [Account]
}
extension FamiliarAccounts: Sendable {}

View file

@ -27,3 +27,5 @@ public struct AppAccount: Codable, Identifiable, Hashable {
self.oauthToken = oauthToken
}
}
extension AppAccount: Sendable {}

View file

@ -11,3 +11,5 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
public let type: String
public let image: URL?
}
extension Card: Sendable {}

View file

@ -41,3 +41,5 @@ public struct ConsolidatedNotification: Identifiable {
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
}
}
extension ConsolidatedNotification: Sendable {}

View file

@ -22,3 +22,5 @@ public struct Conversation: Identifiable, Decodable, Hashable, Equatable {
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
}
}
extension Conversation: Sendable {}

View file

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

View file

@ -9,3 +9,5 @@ public struct InstanceApp: Codable, Identifiable {
public let clientSecret: String
public let vapidKey: String?
}
extension InstanceApp: Sendable {}

View file

@ -18,3 +18,5 @@ public struct Language: Identifiable, Equatable, Hashable {
)
}
}
extension Language: Sendable {}

View file

@ -5,3 +5,5 @@ public struct List: Codable, Identifiable, Equatable, Hashable {
public let title: String
public let repliesPolicy: String
}
extension List: Sendable {}

View file

@ -21,3 +21,5 @@ public struct MastodonPushNotification: Codable {
case body
}
}
extension MastodonPushNotification: Sendable {}

View file

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

View file

@ -6,3 +6,5 @@ public struct Mention: Codable, Equatable, Hashable {
public let url: URL
public let acct: String
}
extension Mention: Sendable {}

View file

@ -23,3 +23,6 @@ public struct Notification: Decodable, Identifiable, Equatable {
status: .placeholder())
}
}
extension Notification: Sendable {}
extension Notification.NotificationType: Sendable {}

View file

@ -48,3 +48,7 @@ public struct NullableString: Codable, Equatable, Hashable {
}
}
}
extension Poll: Sendable {}
extension Poll.Option: Sendable {}
extension NullableString: Sendable {}

View file

@ -15,3 +15,6 @@ public struct PushSubscription: Identifiable, Decodable {
public let serverKey: String
public let alerts: Alerts
}
extension PushSubscription: Sendable {}
extension PushSubscription.Alerts: Sendable {}

View file

@ -50,3 +50,5 @@ public extension Relationship {
notifying = try values.decodeIfPresent(Bool.self, forKey: .notifying) ?? false
}
}
extension Relationship: Sendable {}

View file

@ -14,3 +14,5 @@ public struct SearchResults: Decodable {
accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty
}
}
extension SearchResults: Sendable {}

View file

@ -3,3 +3,5 @@ import Foundation
public struct ServerError: Decodable, Error {
public let error: String?
}
extension ServerError: Sendable {}

View file

@ -33,3 +33,6 @@ public struct ServerPreferences: Decodable {
case autoExpandSpoilers = "reading:expand:spoilers"
}
}
extension ServerPreferences: Sendable {}
extension ServerPreferences.AutoExpandMedia: Sendable {}

View file

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

View file

@ -8,3 +8,5 @@ public struct StatusContext: Decodable {
.init(ancestors: [], descendants: [])
}
}
extension StatusContext: Sendable {}

View file

@ -9,3 +9,5 @@ public struct StatusHistory: Decodable, Identifiable {
public let createdAt: ServerDate
public let emojis: [Emoji]
}
extension StatusHistory: Sendable {}

View file

@ -11,3 +11,5 @@ public struct StatusTranslation: Decodable {
self.provider = provider
}
}
extension StatusTranslation: Sendable {}

View file

@ -58,3 +58,7 @@ public struct FeaturedTag: Codable, Identifiable {
}
}
}
extension Tag: Sendable {}
extension Tag.History: Sendable {}
extension FeaturedTag: Sendable {}

View file

@ -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<String>
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<Critical>
private struct Critical: Sendable {
/// Only used as a transitionary app while in the oauth flow.
var oauthApp: InstanceApp?
var oauthToken: OauthToken?
var connections: Set<String> = []
}
public var isAuth: Bool {
oauthToken != nil
critical.withLock { $0.oauthToken != nil }
}
public var connections: Set<String> {
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 {}

View file

@ -16,3 +16,5 @@ public struct LinkHandler {
return nil
}
}
extension LinkHandler: Sendable {}

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import Combine
import DesignSystem
import Env
import Models

View file

@ -1,3 +1,4 @@
import Combine
import Models
import SwiftUI

View file

@ -1,3 +1,4 @@
import Combine
import Models
import Network
import SwiftUI

View file

@ -1,3 +1,4 @@
import Combine
import Env
import Models
import NaturalLanguage

View file

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

View file

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