A lightweight, thread-safe, reactive state management library for Swift — built with Swift Concurrency, Combine compatibility, and high-end scalability in mind.
Signal.swift was born out of the need for a minimal yet powerful reactive layer suitable for large-scale iOS applications — like social networks or dating apps — without the overhead of RxSwift or Combine-only constraints.
Designed by @ranjanakarsh, it blends:
- Actor-based safety with Swift Concurrency
- Multicast emissions with weak references
- Type erasure, Combine bridging, and value replay
- Debouncing, throttling, and event-state models
- Debug tracing and performance logging
- Actor-isolated signals for race-free data propagation
- Auto-cleanup of deallocated subscribers
- Built entirely with actor isolation for thread safety
- async/await-based subsription model
emit,complete,failfor lifecycle awareness- Type-safe events:
.next,.completed,.failed - Value replay with configurable buffer size (
ValueSignal) - Throttled and debounced emissions
- Combine-compatible
.publisher - Token-based unsubscribe system
- Lightweight — no third-party dependencies required (except
swift-atomics)
.package(url: "https://github.com/ranjanakarsh/Signal.swift.git", from: "1.0.0")Then import:
import SignalBasic Usage
let signal = Signal<String>()
let token = await signal.subscribe(owner: self) { value in
print("Received:", value)
}
signal.emit("Hello world!")Note: all subscribe(...) methods are now async to support actor isolation and concurrency safety. Use await when subscribing to signals.
await signal.subscribeEvent(owner: self) { event in
switch event {
case .next(let value): print("Received:", value)
case .completed: print("Signal completed")
case .failed(let error): print("Error:", error)
}
}
signal.emit("final value")
signal.complete()let valueSignal = ValueSignal<Int>(replayCount: 2)
valueSignal.emit(1)
valueSignal.emit(2)
await valueSignal.subscribe(owner: self) { value in
print("Got replayed value:", value)
}let publisher = await signal.publisher
let cancellable = publisher.sink { value in
print("Combine received:", value)
}Signal Types
| Type | Description |
|---|---|
Signal<T> |
Actor-isolated broadcast channel, async-safe subscriptions |
ValueSignal<T> |
Cached value signal with replayCount, built on Signal |
AnySignal<T> |
Type-erased wrapper supporting async subscribe |
SignalResult<T, E> |
Convenience enum for result-based signaling |
Enable debug tracking:
signal.setDebugOptions(Signal.DebugOptions(
name: "AuthSignal",
loggingEnabled: true
))Support for os_signpost tracing is included.
- Favor Swift-native concurrency over legacy locks
- Maintain decoupled logic with high composability
- Provide low-cost reactivity for UI and real-time systems
- Keep APIs simple and intuitive, but flexible
- Signal-to-async/await stream bridging
- SwiftUI property wrappers
- ReplaySubject-style hot signals
- DevTools visual debugging inspector
- SignalGroup for bulk coordination
Want to help? Contributions are welcome!
- Open an issue or submit a feature request
- Fort the repo and create a PR
- Write tests for new features
Please ensure your changes are covered with tests and follow Swift style guides.
This project draws conceptual inspiration form:
- Combine
- RxSwift
- The Composable Architecture (TCA)
- SignalKit
MIT @ Ranjan Akarsh