Using Observations allows to have observable actors!?

We’re working with an older codebase that still relies on classes and callback-based patterns—typical of pre-concurrency Swift. We’re now in the process of migrating those to actors, which internally use SerialDispatchQueueExecutors.

With Swift 6.2 and the new Observation system, it seems data of actors can now be used with Observations, which would be a great improvement—if I’m understanding this correctly. Am I missing anything?

Attached is a simplified example of how we’re thinking about structuring the code.

Example Code:

import Foundation
import Observation


#if os(Linux) || os(Android) || os(Windows) || os(WASI)
typealias DispatchSerialQueue = DispatchQueue
final class DispatchQueueExecutor: SerialExecutor
{
     private let queue: DispatchQueue

     init(queue: DispatchQueue) {
         self.queue = queue
     }

     func enqueue(_ job: UnownedJob) {
         self.queue.async {
             job.runSynchronously(on: self.asUnownedSerialExecutor())
         }
     }

     func asUnownedSerialExecutor() -> UnownedSerialExecutor {
         UnownedSerialExecutor(ordinary: self)
     }

    func checkIsolated() {
        dispatchPrecondition(condition: .onQueue(self.queue))
    }
}
#endif



struct Contact : CustomStringConvertible
{
    var name: String
    var description : String { name }
}

@Observable
class ContactStore
{
    var contacts:[Contact] = []
}

actor ActorManagerA
{
    let queue: DispatchSerialQueue = .init(label: "MYActorManagerQueue")
    #if os(Linux) || os(Android) || os(Windows) || os(WASI)
    private let executor: DispatchQueueExecutor
    #endif

    nonisolated var unownedExecutor: UnownedSerialExecutor
    {
        #if os(Linux) || os(Android) || os(Windows) || os(WASI)
        executor.asUnownedSerialExecutor()
        #else
        queue.asUnownedSerialExecutor()
        #endif
    }

    init()
    {
        #if os(Linux) || os(Android) || os(Windows) || os(WASI)
        self.executor = DispatchQueueExecutor(queue: queue)
        #endif
    }


    public let contactStore: ContactStore = ContactStore()

    func observableContactStream() -> Observations<[Contact],Never>
    {
        Observations { self.contactStore.contacts }
    }
}

extension ActorManagerA // normal
{
    func addContact(_ contact: Contact)
    {
        contactStore.contacts.append(contact)
    }

    var names: [String]
    {
        contactStore.contacts.map { $0.name }
    }
}

// synced methods - we use queue.sync_ifneeded {} instead
// of just queue.sync as we have such an extension on DispatchQueue

extension ActorManagerA
{
    nonisolated var syncNames: [String]
    {
        queue.sync
        {
            self.assumeIsolated
            {
                isolatedSelf in

                isolatedSelf.contactStore.contacts.map( \.name )
            }
        }
    }

    nonisolated func syncAddContact(_ contact: Contact)
    {
        queue.sync
        {
            self.assumeIsolated  {
                isolatedSelf in

                isolatedSelf.contactStore.contacts.append(contact)
            }
        }
    }
}

let manager = ActorManagerA()
print("a contacts:\(await manager.names.joined(separator: ", "))")

Task // could also be .detached
{
    for await contacts in await manager.observableContactStream()
    {
        print("contacts controller got change:\(contacts.map{ $0.name }.joined(separator: ", "))")
    }
}

Task.detached
{
    await manager.addContact(Contact(name:"Alice"))
    await manager.addContact(Contact(name:"Bob"))
}
manager.syncAddContact(Contact(name:"Charlie"))

// wait for everything to finish
try await Task.sleep(for:.seconds(0.01))

print("b contacts:\(manager.syncNames.joined(separator: ", "))")

can be run with:

docker run --rm -v "$PWD:/home" -w /home -ti swiftlang/swift:nightly-main swift -swift-version 6 test.swift