Modern async/await observation plugin for SundialKit with actor-based concurrency safety.
- Overview
- Why Choose SundialKitStream
- Key Features
- Requirements
- Installation
- Usage
- Architecture
- Comparison with SundialKitCombine
- Documentation
- Related Packages
- License
SundialKitStream provides actor-based observers that deliver state updates via AsyncStream APIs. This plugin is designed for Swift 6.1+ projects using modern concurrency patterns, offering natural thread safety through Swift's actor isolation model and seamless integration with async/await code.
SundialKitCombine is a part of SundialKit - a reactive communications library for Apple platforms.
If you're building a modern Swift application that embraces async/await and structured concurrency, SundialKitStream is the ideal choice. It leverages Swift's actor isolation to provide thread-safe state management without locks, mutexes, or manual synchronization. The AsyncStream-based APIs integrate naturally with async/await code, making it easy to consume network and connectivity updates in Task contexts.
Choose SundialKitStream when you:
- Want to use modern async/await patterns throughout your app
- Need actor-based thread safety without @unchecked Sendable
- Prefer consuming updates with
for awaitloops - Target iOS 16+ / watchOS 9+ / tvOS 16+ / macOS 13+
- Value compile-time concurrency safety with Swift 6.1 strict mode
- Actor Isolation: Natural thread safety without locks or manual synchronization
- AsyncStream APIs: Consume state updates with
for awaitloops in async contexts - Swift 6.1 Strict Concurrency: Zero
@unchecked Sendableconformances - everything is properly isolated - Composable: Works seamlessly with SundialKitNetwork and SundialKitConnectivity
- Structured Concurrency: AsyncStreams integrate naturally with Task hierarchies and cancellation
- Swift: 6.1+
- Xcode: 16.0+
- Platforms:
- iOS 16+
- watchOS 9+
- tvOS 16+
- macOS 13+
Add SundialKitStream to your Package.swift:
let package = Package(
name: "YourPackage",
platforms: [.iOS(.v16), .watchOS(.v9), .tvOS(.v16), .macOS(.v13)],
dependencies: [
.package(url: "https://github.com/brightdigit/SundialKit.git", from: "2.0.0-alpha.1"),
.package(url: "https://github.com/brightdigit/SundialKitStream.git", from: "1.0.0-alpha.1")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "SundialKitStream", package: "SundialKitStream"),
.product(name: "SundialKitNetwork", package: "SundialKit"), // For network monitoring
.product(name: "SundialKitConnectivity", package: "SundialKit") // For WatchConnectivity
]
)
]
)Monitor network connectivity changes using the actor-based NetworkObserver. The observer tracks network path status, connection quality (expensive, constrained), and optionally performs periodic connectivity verification with custom ping implementations.
import SundialKitStream
import SundialKitNetwork
import SwiftUI
@MainActor
@Observable
class NetworkModel {
var pathStatus: PathStatus = .unknown
var isExpensive: Bool = false
var isConstrained: Bool = false
private let observer = NetworkObserver(
monitor: NWPathMonitorAdapter(),
ping: nil
)
func start() {
observer.start(queue: .global())
// Listen to path status updates
Task {
for await status in observer.pathStatusStream {
self.pathStatus = status
}
}
// Listen to expensive network status
Task {
for await expensive in observer.isExpensiveStream {
self.isExpensive = expensive
}
}
// Listen to constrained network status
Task {
for await constrained in observer.isConstrainedStream {
self.isConstrained = constrained
}
}
}
}
// Use in SwiftUI
struct NetworkView: View {
@State private var model = NetworkModel()
var body: some View {
VStack {
Text("Status: \(model.pathStatus.description)")
Text("Expensive: \(model.isExpensive ? "Yes" : "No")")
Text("Constrained: \(model.isConstrained ? "Yes" : "No")")
}
.task {
model.start()
}
}
}The NWPathMonitorAdapter wraps Apple's NWPathMonitor from the Network framework, providing updates whenever the network path changes (WiFi connects/disconnects, cellular becomes available, etc.).
The PathStatus enum represents the current state of the network path:
.satisfied- Network is available and ready to use.unsatisfied- No network connectivity.requiresConnection- Network may be available but requires user action (e.g., connecting to WiFi).unknown- Initial state before first update
Beyond basic connectivity, you can track whether the current network connection is expensive (cellular data) or constrained (low data mode):
// Monitor all quality indicators
Task {
for await isExpensive in observer.isExpensiveStream {
if isExpensive {
// User is on cellular data - consider reducing data usage
print("Warning: Using cellular data")
}
}
}
Task {
for await isConstrained in observer.isConstrainedStream {
if isConstrained {
// User has Low Data Mode enabled - minimize data usage
print("Low Data Mode active")
}
}
}This information helps you build adaptive applications that respect users' data plans and preferences.
Communicate between iPhone and Apple Watch using the actor-based ConnectivityObserver. The observer manages the WatchConnectivity session lifecycle, handles automatic transport selection, and provides type-safe messaging through AsyncStream APIs.
Before sending or receiving messages, you must activate the WatchConnectivity session:
import SundialKitStream
import SundialKitConnectivity
actor WatchCommunicator {
private let observer = ConnectivityObserver()
func activate() async throws {
try await observer.activate()
}
func listenForMessages() async {
for await result in observer.messageStream() {
switch result.context {
case .replyWith(let handler):
print("Received: \(result.message)")
handler(["response": "acknowledged"])
case .applicationContext:
print("Context update: \(result.message)")
}
}
}
func sendMessage(_ message: ConnectivityMessage) async throws -> ConnectivitySendResult {
try await observer.sendMessage(message)
}
}The activate() method initializes the WatchConnectivity session and waits for it to become ready. Once activated, you can send and receive messages.
Messages arrive with different contexts that indicate how they should be handled:
.replyWith(handler)- Interactive message expecting an immediate reply. Use the handler to send a response..applicationContext- Background state update delivered when devices can communicate. No reply expected.
This distinction helps you build responsive communication patterns - interactive messages for user-initiated actions, context updates for background state synchronization.
Use the connectivity observer in SwiftUI with the @Observable macro:
import SwiftUI
import SundialKitStream
import SundialKitConnectivity
@MainActor
@Observable
class WatchModel {
var activationState: ActivationState = .notActivated
var isReachable: Bool = false
var lastMessage: String = ""
private let observer = ConnectivityObserver()
func start() async throws {
try await observer.activate()
// Monitor activation state
Task {
for await state in observer.activationStates() {
self.activationState = state
}
}
// Monitor reachability
Task {
for await reachable in observer.reachabilityStream() {
self.isReachable = reachable
}
}
// Listen for messages
Task {
for await result in observer.messageStream() {
if let text = result.message["text"] as? String {
self.lastMessage = text
}
}
}
}
func sendMessage(_ text: String) async throws {
let result = try await observer.sendMessage(["text": text])
print("Sent via: \(result.context)")
}
}
struct WatchView: View {
@State private var model = WatchModel()
@State private var messageText = ""
var body: some View {
VStack {
Text("Session: \(model.activationState.description)")
Text("Reachable: \(model.isReachable ? "Yes" : "No")")
TextField("Message", text: $messageText)
Button("Send") {
Task {
try? await model.sendMessage(messageText)
}
}
.disabled(!model.isReachable)
Text("Last message: \(model.lastMessage)")
}
.task {
try? await model.start()
}
}
}For type-safe messaging, use the Messagable protocol with MessageDecoder:
import SundialKitConnectivity
// Define a typed message
struct ColorMessage: Messagable {
let red: Double
let green: Double
let blue: Double
static let key = "color"
init(from parameters: [String: any Sendable]) throws {
guard let red = parameters["red"] as? Double,
let green = parameters["green"] as? Double,
let blue = parameters["blue"] as? Double else {
throw SerializationError.missingField("color components")
}
self.red = red
self.green = green
self.blue = blue
}
func parameters() -> [String: any Sendable] {
["red": red, "green": green, "blue": blue]
}
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
}
// Configure observer with MessageDecoder
actor WatchCommunicator {
private let observer = ConnectivityObserver(
messageDecoder: MessageDecoder(messagableTypes: [ColorMessage.self])
)
func listenForColorMessages() async throws {
for await message in observer.typedMessageStream() {
if let colorMsg = message as? ColorMessage {
print("Received color: RGB(\(colorMsg.red), \(colorMsg.green), \(colorMsg.blue))")
}
}
}
func sendColor(_ color: ColorMessage) async throws {
let result = try await observer.send(color)
print("Color sent via: \(result.context)")
}
}SundialKitStream is part of SundialKit's three-layer architecture:
Layer 1: Core Protocols (SundialKitCore, SundialKitNetwork, SundialKitConnectivity)
- Protocol-based abstractions over Apple's Network and WatchConnectivity frameworks
- No observer patterns - pure wrappers
Layer 2: Observation Plugin (SundialKitStream - this package)
- Actor-based observers with AsyncStream APIs
- Modern async/await patterns
- Natural Sendable conformance without @unchecked
| Feature | SundialKitStream | SundialKitCombine |
|---|---|---|
| Concurrency Model | Actor-based | @MainActor-based |
| State Updates | AsyncStream | @Published properties |
| Thread Safety | Actor isolation | @MainActor isolation |
| Platform Support | iOS 16+, watchOS 9+, tvOS 16+, macOS 13+ | iOS 13+, watchOS 6+, tvOS 13+, macOS 10.15+ |
| Use Case | Modern async/await apps | Combine-based apps, SwiftUI with ObservableObject |
For comprehensive documentation, see:
- SundialKit - Main package with core protocols and implementations
- SundialKitCombine - Combine-based plugin
This code is distributed under the MIT license. See the LICENSE file for more info.
