11.6 C
New York
Tuesday, March 11, 2025

ios – Find out how to implement thread-safe property wrapper notifications throughout completely different contexts in Swift?


I’m attempting to create a property wrapper that that may handle shared state throughout any context, which might get notified if modifications occur from someplace else.

I am utilizing mutex, and getting and setting values works nice. Nevertheless, I can not discover a method to create an observer sample that the property wrappers can use.

The issue is that I can’t set off a notification from a special thread/context, and have that notification get known as on the right thread of the guardian object that the property wrapper is used inside.

I would really like the property wrapper to work from wherever: a SwiftUI view, an actor, or from a category that’s created within the background. The notification ideally would get known as synchronously if triggered from the identical thread or actor, or in any other case asynchronously. I don’t have to fret about race circumstances from the notification as a result of the state solely wants to achieve eventuall consistency.

Here is the simplified pseudo code of what I am attempting to perform:

// A single supply of fact storage container.
ultimate class MemoryShared: Sendable {
    let state = Mutex(0)

    func withLock(_ motion: (inout Worth) -> Void) {
        state.withLock(motion)
        notifyObservers()
    }

    func get() -> Worth
    func notifyObservers()
    func addObserver()
}

// Some shared state used throughout the app
static let globalCount = MemoryShared(0)

// A property wrapper to entry the shared state and obtain modifications
@propertyWrapper
struct SharedState {
    public var wrappedValue: T {
        get { state.get() }
        nonmutating set { // Cannot set instantly }
    }

    var writer: Writer {}

    init(state: MemoryShared) {
        // ...
    }
}

// I might like to make use of it in a number of locations:

@Observable
class MyObservable {
    @SharedState(globalCount)
    var depend: Int
}

actor MyBackgroundActor {
    @SharedState(globalCount)
    var depend: Int
}

@MainActor
struct MyView: View {
    @SharedState(globalCount)
    var depend: Int
}

What I’ve Tried

All the examples beneath are utilizing the property wrapper inside a @MainActor class. Nevertheless the identical challenge occurs it doesn’t matter what context I take advantage of the wrapper in: The notification callback isn’t known as on the context the property wrapper was created with.

I’ve tried utilizing @remoted(any) to seize the context of the wrapper and reserve it to be known as inside the state in with unchecked sendable, which doesn’t work:

ultimate class MemoryShared: Sendable {
    // Shops the callback for later.
    public func subscribe(callback: @escaping @remoted(any) (Worth) -> Void) -> Subscription 
}

@propertyWrapper
struct SharedState {
    init(state: MemoryShared) {
        MainActor.assertIsolated() // Works!
        state.subscribe {
            MainActor.assertIsolated() // Fails
            self.writer.ship()
        }
    }
}

I’ve tried capturing the isolation inside a activity with AsyncStream. This truly compiles with no sendable points, however nonetheless fails:

@propertyWrapper
struct SharedState {    
    init(isolation: remoted (any Actor)? = #isolation, state: MemoryShared) {
        let (taskStream, continuation) = AsyncStream.makeStream()
        // The shared state sends new values to the continuation. 
        subscription = state.subscribe(continuation: continuation)
        
        MainActor.assertIsolated() // Works!
        
        let activity = Process {
            _ = isolation
            for await worth in taskStream {
                _ = isolation
                MainActor.assertIsolated() // Fails
            }
        }
    }
}

I’ve tried utilizing a number of mix topics and publishers:

ultimate class MemoryShared: Sendable {
    let topic: PassthroughSubject // ...
    var writer: Writer {} // ...
}

@propertyWrapper
ultimate class SharedState {
    var localSubject: Topic

    init(state: MemoryShared) {
        MainActor.assertIsolated() // Works!
        deal with = localSubject.sink {
            MainActor.assertIsolated() // Fails
        }

        stateHandle = state.writer.subscribe(localSubject)
    }
}

I’ve additionally tried:

  1. Utilizing NotificationCenter
  2. Making the property wrapper a category
  3. Utilizing NSKeyValueObserving
  4. Utilizing a field class that’s saved inside the wrapper.
  5. Utilizing @_inheritActorContext.

All of those don’t work, as a result of the occasion isn’t known as from the thread the property wrapper resides in.

Is it potential in any respect to create an remark system that notifies the observer from the identical context as the place the observer was created?

Thansk!

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles