Dependency with main actor-isolated initializer requirements like FeedbackGeneratorClient from isowords #22
-
|
Hello, I was trying to extract public struct FeedbackGeneratorClient: Sendable {
public internal(set) var prepare: @Sendable () async -> Void
public internal(set) var generate: @Sendable () async -> Void
public init(
prepare: @escaping @Sendable () async -> Void,
generate: @escaping @Sendable () async -> Void
) {
self.prepare = prepare
self.generate = generate
}
}
extension DependencyValues {
public var feedbackGeneratorClient: FeedbackGeneratorClient {
get { self[FeedbackGeneratorClient.self] }
set { self[FeedbackGeneratorClient.self] = newValue }
}
}And the live implementation looks like this: extension FeedbackGeneratorClient: DependencyKey {
public static let liveValue = {
let selectionFeedbackGenerator = UISelectionFeedbackGenerator() // 🔴 Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context
return Self(
prepare: { await selectionFeedbackGenerator.prepare() },
generate: { await selectionFeedbackGenerator.selectionChanged() }
)
}()
}However if you have Maybe something like a |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 13 replies
-
|
Hey @ph1ps, you're right, it is indeed really annoying to wrap main actor stuff with the protocol requirement getting in the way. public final class MainActorIsolated<Value: AnyObject>: @unchecked Sendable {
@MainActor
public private(set) lazy var value: Value = initialValue()
private let initialValue: @MainActor @Sendable () -> Value
public init(_ initialValue: @MainActor @Sendable @escaping () -> Value) {
self.initialValue = initialValue
}
@MainActor
public func withValue<T: Sendable>(
_ operation: @MainActor (inout Value) throws -> T
) rethrows -> T {
return try operation(&value)
}
@MainActor
public subscript<Subject: Sendable>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
self.value[keyPath: keyPath]
}
}Because the value is only lazily created in a I was planning to add this utility to |
Beta Was this translation helpful? Give feedback.
-
Hello all. Been working on a dependency with the same issue: needing to initialize resources on the main actor. Unfortunately, the quoted suggestion is now warning for me on Xcode 15.3 / Swift 5.10 / @MainActor
public final class MainActorIsolated<Value>: Sendable {
public lazy var value: Value = initialValue() // ⚠️ Non-sendable type '@MainActor () -> Value' in asynchronous access to main actor-isolated property 'initialValue' cannot cross actor boundary
private let initialValue: @MainActor () -> Value
nonisolated public init(initialValue: @MainActor @escaping () -> Value) {
self.initialValue = initialValue // ⚠️ Main actor-isolated property 'initialValue' can not be mutated from a non-isolated context; this is an error in Swift 6
}
}Was playing around, trying to come up with a solution to this problem. What about something like this? Gives me no warnings. struct LocationClient: Sendable {
var requestLocation: @Sendable () async -> Void
// etc
}
extension LocationClient: DependencyKey {
public static var liveValue: Self {
Self(
requestLocation: { @MainActor in
Self.manager.requestLocation()
}
// etc
)
}
@MainActor
private static let manager = {
let manager = CLLocationManager()
// configure resources if necessary
return manager
}()
}Not sure if there's a way to extract the main-actor-static isolation into a wrapper type that doesn't warn; could save needing to annotate every endpoint closure Got the idea to store resources as statics on the client from isowords' Game Center client. |
Beta Was this translation helpful? Give feedback.
-
|
Adding on to an old discussion, but this solution seems to work in Swift 6 language mode. I'm using this solution to share a TCA store via I introduced this type which is not isolated: struct Provider<T>: Sendable {
private let provide: @MainActor () -> T
init(_ provide: @autoclosure @MainActor @escaping () -> T) {
self.provide = provide
}
@MainActor func callAsFunction() -> T {
provide()
}
}Then I register the dependency like any other. extension DependencyValues {
var feature: Provider<StoreOf<Feature>> {
get { self[Feature.self] }
set { self[Feature.self] = newValue }
}
}
extension Feature: DependencyKey {
// Need to use a static instance for the live value
@MainActor private static let shared = StoreOf<Feature>(initialState: Feature.State()) {
Feature()
}
static let liveValue: Provider<StoreOf<Feature>> = Provider(Self.shared)
}And finally, it can be used anywhere, but the key path must be invoked (i.e. // Usage off MainActor
final class MainActorDependency {
@Dependency(\.feature()) var feature
func testing() async {
print(await feature.state)
await feature(.action)
}
}
// Usage on MainActor
@MainActor
final class SimpleUseFromMain {
@Dependency(\.feature()) var feature
func executesOnMain() {
print(feature.state)
feature.send(.action)
}
} |
Beta Was this translation helpful? Give feedback.

Hey @ph1ps, you're right, it is indeed really annoying to wrap main actor stuff with the protocol requirement getting in the way.
I have a
MainActorIsolatedwrapper that I'm toying with to tackle these issues: