Introducing Swift Streamable Actors: Reactive State for Swift Actors

Published by malhal on

I’m thrilled to announce the official release of Swift Streamable Actors, a new Swift macro library that brings AsyncStream-based reactivity to your Swift Actors. If you’ve ever wished you could observe changes to an actor’s properties as easily as an @Observable object, this library is for you.

The Inspiration: Manual Streamable Properties

Before diving into the macro, let’s look at the problem it solves. Swift Actors are fantastic for managing mutable state safely in concurrent environments. However, sharing that state reactively with the rest of your application can involve a fair bit of boilerplate.

My journey to this macro started with a common pattern I found myself writing whenever I needed to stream an actor’s property:

Swift

actor MyActor {
    private var countObservers: [UUID: AsyncStream<Int>.Continuation] = [:]
    
    var count: Int = 0 {
        didSet {
            countObservers.values.forEach { $0.yield(count) }
        }
    }
    
    static func countStream(for actor: MyActor) async -> AsyncStream<Int> {
        let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
        let id = UUID()
        
        await actor.register(id: id, continuation: continuation)
        
        continuation.onTermination = { _ in
            Task { await actor.unregister(id: id) }
        }
        
        return stream
    }
    
    private func register(id: UUID, continuation: AsyncStream<Int>.Continuation) {
        countObservers[id] = continuation
        continuation.yield(count) // Initial value
    }
    
    private func unregister(id: UUID) {
        countObservers.removeValue(forKey: id)
    }
    
    func increment() {
        count += 1
    }
}

This works! It’s robust, concurrent-safe, and provides a beautiful AsyncStream interface. But imagine doing this for five, ten, or even more properties in a complex actor. The boilerplate quickly becomes overwhelming, obscuring the actual business logic.

Enter the @Streamable Macro!

This is precisely the kind of repetitive, predictable code generation that Swift macros excel at. With swift-streamable-actors, you can achieve the same reactive capabilities with a single attribute:

Swift

import StreamableActors

@Streamable
actor SensorManager {
    var reading: Double = 0.0
    var status: String = "Calibrating"
    
    // Use @StreamableIgnored for properties you don't want to stream
    @StreamableIgnored
    var internalID: UUID = UUID() 
    
    func update(to value: Double) {
        reading = value
        status = "Active"
    }
}

And observing changes is just as clean:

Swift

let sensors = SensorManager()

Task {
    // Access the stream via the Actor type, passing the instance
    let readings = await SensorManager.readingStream(for: sensors)
    
    for await value in readings {
        print("New sensor reading: \(value)")
    }
}

🧠 The Design Philosophy: Shadow Storage & Robustness

You might wonder how this magic happens. Instead of relying on didSet (which can sometimes lead to re-entrancy issues with complex observers), swift-streamable-actors uses a Shadow Storage pattern, similar to how @Observable works:

  1. Storage Redirection: Your original property (e.g., var reading: Double) is effectively renamed to a private backing field (e.g., _reading).
  2. Accessor Injection: The macro transforms the original property into a computed property. The set accessor updates the backing field and then iterates through a private registry of AsyncStream.Continuation objects to yieldthe new value.
  3. Static Stream Factory: A static method is generated that handles creating the AsyncStream, generating a unique UUID for the subscriber, and safely registering it within the actor’s isolated context.
  4. Automatic Cleanup: Crucially, each stream is configured with an onTermination handler. When a consumer stops listening, it triggers a background Task to call back into the actor and remove the specific observer from the registry, preventing memory leaks.

🚀 Get Started Today!

You can add swift-streamable-actors to your project by adding it as a Swift Package Manager dependency:

Swift

dependencies: [
    .package(url: "https://github.com/malhal/swift-streamable-actors", from: "1.0.1")
]

Be sure to check out the GitHub repository for full documentation, examples, and to contribute! I’m excited to see how this simplifies your concurrent Swift applications. Feel free to reach out with feedback or questions!