I'd like to share some experimental work I've done exploring reactive programming patterns within the Swift Concurrency model. During my research, I identified certain gaps between traditional reactive patterns and the new async/await paradigm, and I've proposed experimental implementations to address these challenges.
My study repository is available at: GitHub - o-nnerb/swift-async-stream: Experimental implementations of AsyncSignal, ValueSubject, PassthroughSubject and AsyncExpectation
Context and Motivation
Swift Concurrency has brought significant improvements to asynchronous programming in Swift, yet I noticed that some common patterns from frameworks like Combine lack direct equivalents in this new paradigm. Specifically, I explored three areas I consider important for discussion:
- Synchronization mechanisms beyond
actorfor complex scenarios - Implementation of reactive subjects (
ValueSubjectandPassthroughSubject) compatible with async sequences - An asynchronous testing utility similar to
XCTestExpectation, but compatible with the new Swift Testing framework
1. AsyncLock: Granular Synchronization in Asynchronous Contexts
While actor is the recommended solution for state isolation in Swift Concurrency, there are scenarios requiring more granular control over synchronization. Consider a method that must guarantee mutual exclusion throughout its entire executionāincluding asynchronous calls:
actor SomeManager {
private(set) var value = 0
func doSomething(_ block: @Sendable () async -> Void) async {
value += 1
await block()
value += 2
}
func updateValue(_ value: Int) {
self.value = value
}
}
In this example, there's no guarantee of atomicity during doSomethingāother operations may interleave during the suspension inside block(). To address this, I implemented an AsyncLock enabling explicit synchronization:
final class SomeManager: @unchecked Sendable {
private let lock = AsyncLock()
private var _value = 0
var value: Int {
get async {
await lock.withLock { _value }
}
}
func doSomething(_ block: @Sendable () async -> Void) async {
await lock.withLock {
_value += 1
await block()
_value += 2
}
}
func updateValue(_ value: Int) async {
await lock.withLock { _value = value }
}
}
This approach ensures no other operation can modify _value while doSomething is runningāeven across async suspensions. Iād appreciate community input on whether this is a valid approach or if there are more idiomatic alternatives within Swift Concurrency.
2. Reactive Subjects for AsyncSequence
The swift-async-algorithms package provides excellent tools for working with async sequences, but I noticed it lacks direct equivalents to Combineās Subject types. The provided AsyncChannel has important limitations:
- Supports only a single consumer
- Doesnāt retain the current value for new subscribers
In my experiments, I implemented ValueSubject and PassthroughSubject as async sequences supporting multiple consumers:
let valueSubject = ValueSubject(1)
Task {
for await index in valueSubject {
print("Task 1 received: \(index)")
}
}
Task {
for await index in valueSubject {
print("Task 2 received: \(index)")
}
}
// Both tasks receive all values
valueSubject.value = 5
A critical aspect of this implementation is managing reference cycles. To address this, I introduced AnyAsyncSequence, which "erases" subscriber references back to the producer:
let erasedSubject = valueSubject.eraseToAnyAsyncSequence()
While functional in my experimental scenarios, I recognize this approach may have performance or safety implications requiring deeper community review.
3. AsyncExpectation for Swift Testing
The new Swift Testing framework currently lacks a direct equivalent to XCTestExpectation, making it difficult to write tests that depend on asynchronous conditions. I implemented AsyncExpectation to fill this gap:
import SwiftAsyncTesting
func testAsyncOperation() async throws {
let expectation = AsyncExpectation(description: "Async operation completes")
Task {
await someAsyncFunction()
expectation.fulfill()
}
try await expectations([expectation], timeout: 10.0)
}
This implementation is compatible with both Swift Testing and XCTest, and provides features such as:
- Support for multiple simultaneous expectations
- Configurable timeout-based waiting
- Support for inverted expectations (which fail when fulfilled)
Conclusion and Next Steps
These implementations emerged as an academic exercise to better understand the nuances of Swift Concurrency and how it interacts with traditional reactive patterns. I recognize that some solutions may not be fully optimized or idiomatic, and Iām actively seeking community feedback on the following questions:
- Are there better alternatives for granular synchronization beyond
actor? - How could
AsyncChannelevolve to efficiently support multiple consumers? - Is there an official plan to bring
XCTestExpectation-like functionality to Swift Testing?
Thank you in advance for any contributions, constructive criticism, or suggestions for improvement. The repository is open for discussions and PRs, and I intend to continue refining these ideas based on community feedback.