Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

How to implement thread-safe property wrapper notifications across different contexts in Swift?

I’m trying to create a property wrapper that that can manage shared state across any context, which can get notified if changes happen from somewhere else.

I'm using mutex, and getting and setting values works great. However, I can't find a way to create an observer pattern that the property wrappers can use.

The problem is that I can’t trigger a notification from a different thread/context, and have that notification get called on the correct thread of the parent object that the property wrapper is used within.

I would like the property wrapper to work from anywhere: a SwiftUI view, an actor, or from a class that is created in the background. The notification preferably would get called synchronously if triggered from the same thread or actor, or otherwise asynchronously. I don’t have to worry about race conditions from the notification because the state only needs to reach eventuall consistency.

Here's the simplified pseudo code of what I'm trying to accomplish:

// A single source of truth storage container.
final class MemoryShared<Value>: Sendable {
    let state = Mutex<Value>(0)

    func withLock(_ action: (inout Value) -> Void) {
        state.withLock(action)
        notifyObservers()
    }

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

// Some shared state used across the app
static let globalCount = MemoryShared<Int>(0)

// A property wrapper to access the shared state and receive changes
@propertyWrapper
struct SharedState<Value> {
    public var wrappedValue: T {
        get { state.get() }
        nonmutating set { // Can't set directly }
    }

    var publisher: Publisher {}

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

// I'd like to use it in multiple places:

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

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

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

What I’ve Tried

All of the examples below are using the property wrapper within a @MainActor class. However the same issue happens no matter what context I use the wrapper in: The notification callback is never called on the context the property wrapper was created with.

I’ve tried using @isolated(any) to capture the context of the wrapper and save it to be called within the state in with unchecked sendable, which doesn’t work:

final class MemoryShared<Value: Sendable>: Sendable {
    // Stores the callback for later.
    public func subscribe(callback: @escaping @isolated(any) (Value) -> Void) -> Subscription 
}

@propertyWrapper
struct SharedState<Value> {
    init(state: MemoryShared<Value>) {
        MainActor.assertIsolated() // Works!
        state.subscribe {
            MainActor.assertIsolated() // Fails
            self.publisher.send()
        }
    }
}

I’ve tried capturing the isolation within a task with AsyncStream. This actually compiles with no sendable issues, but still fails:

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

I’ve tried using multiple combine subjects and publishers:

final class MemoryShared<Value: Sendable>: Sendable {
    let subject: PassthroughSubject<T, Never> // ...
    var publisher: Publisher {} // ...
}

@propertyWrapper
final class SharedState<Value> {
    var localSubject: Subject

    init(state: MemoryShared<Value>) {
        MainActor.assertIsolated() // Works!
        handle = localSubject.sink {
            MainActor.assertIsolated() // Fails
        }

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

I’ve also tried:

  1. Using NotificationCenter
  2. Making the property wrapper a class
  3. Using NSKeyValueObserving
  4. Using a box class that is stored within the wrapper.
  5. Using @_inheritActorContext.

All of these don’t work, because the event is never called from the thread the property wrapper resides in.

Is it possible at all to create an observation system that notifies the observer from the same context as where the observer was created?

Any help would be greatly appreciated!

Answered by DTS Engineer in 828621022
Is it possible at all to create an observation system that notifies the observer from the same context as where the observer was created?

That’s gonna be very challenging in the general case. The problem is one of defining what you mean by “context”. There are many different ways that Apple platforms interact with execution contexts:

  • Threads

  • Run loops

  • Dispatch queues

  • Swift concurrency

  • And, more specifically, Swift actors

My reading of your problem spec if that you want to capture the current context and then, at some arbitrary time in the future, execute code in that context. Is that right?

If so, many of these contexts simply don’t support that notion. For example, you can’t do that with threads [1] or Dispatch queues. However, I suspect you’re primarily concerned with Swift concurrency, in which case you have more options.

If you’re OK with focusing exclusively on Swift concurrency then I’m gonna suggest that you bounce over to Swift Forums. It’s likely that you’ll get more traction over there.

OTOH, I’m happy to discuss the Apple-specific stuff here, although I suspect that my answers will largely be “No.” )-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Unless you require that the thread run its run loop.

You could perhaps check if the following open source library written by two very renowned swift developers meets your needs or get some inspiration from their code?

Is it possible at all to create an observation system that notifies the observer from the same context as where the observer was created?

That’s gonna be very challenging in the general case. The problem is one of defining what you mean by “context”. There are many different ways that Apple platforms interact with execution contexts:

  • Threads

  • Run loops

  • Dispatch queues

  • Swift concurrency

  • And, more specifically, Swift actors

My reading of your problem spec if that you want to capture the current context and then, at some arbitrary time in the future, execute code in that context. Is that right?

If so, many of these contexts simply don’t support that notion. For example, you can’t do that with threads [1] or Dispatch queues. However, I suspect you’re primarily concerned with Swift concurrency, in which case you have more options.

If you’re OK with focusing exclusively on Swift concurrency then I’m gonna suggest that you bounce over to Swift Forums. It’s likely that you’ll get more traction over there.

OTOH, I’m happy to discuss the Apple-specific stuff here, although I suspect that my answers will largely be “No.” )-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Unless you require that the thread run its run loop.

How to implement thread-safe property wrapper notifications across different contexts in Swift?
 
 
Q