Reactive system observers for iOS — connectivity, lifecycle, keyboard, and more.
ForgeObservers exposes the most common iOS system events as AsyncStream values behind clean protocols. Every observer is testable via an injectable NotificationCenter — no UIApplication.shared required, no @testable import tricks.
| Observer | Protocol | Emits |
|---|---|---|
| ConnectivityObserver | ConnectivityObserving |
ConnectivityStatus — network path, interface, expensive/constrained flags |
| AppLifecycleObserver | AppLifecycleObserving |
AppLifecycleState — .active, .inactive, .background |
| KeyboardObserver | KeyboardObserving |
KeyboardState — visibility, height, animation duration |
| AppearanceObserver | AppearanceObserving |
AppAppearance — light/dark mode |
| LocaleObserver | LocaleObserving |
AppLocale — language + region code |
| ProtectedDataObserver | ProtectedDataObserving |
ProtectedDataState — available/unavailable, with waitUntilAvailable() |
| NotificationPermissionObserver | NotificationPermissionObserving |
NotificationPermissionStatus |
- AsyncStream-first — subscribe with
for awaitin a.taskmodifier or inside an actor - Protocol-oriented — each observer has a protocol, making mocking trivial
- Testable by design — notification-based observers accept an injectable
NotificationCenterso tests can pump fake notifications .assign(to:on:)helper — bind anyAsyncSequencedirectly to a property on an object- Zero Combine dependency — pure Swift Concurrency
- iOS 18+
- Swift 6.3+ (Xcode 26 or later)
- File → Add Package Dependencies…
- Paste
https://github.com/stefanprojchev/ForgeObservers.git - Set rule to Up to Next Major from
1.0.0
dependencies: [
.package(url: "https://github.com/stefanprojchev/ForgeObservers.git", from: "1.0.0")
],
targets: [
.target(
name: "YourApp",
dependencies: ["ForgeObservers"]
)
]import ForgeObservers
let connectivity = ConnectivityObserver()
// Sync read of the current status
if connectivity.status.isConnected {
await fetchLatestData()
}
// Subscribe to changes
Task {
for await status in connectivity.statusStream {
print("Connected: \(status.isConnected), via: \(status.interface)")
}
}import SwiftUI
import ForgeObservers
struct ContentView: View {
let lifecycle: AppLifecycleObserving
var body: some View {
FeedView()
.task {
for await state in lifecycle.stateStream {
switch state {
case .active: resumeTimers()
case .background: await saveState()
case .inactive: break
}
}
}
}
}import Testing
import UIKit
@testable import ForgeObservers
@Test
func lifecycleReactsToBackground() async throws {
// Inject a fresh NotificationCenter — no interference from the real one
let center = NotificationCenter()
let observer = AppLifecycleObserver(notificationCenter: center)
center.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
try await Task.sleep(for: .milliseconds(50))
#expect(observer.state == .background)
}Bind any stream directly to a property:
@Observable
final class AppViewModel {
var isOffline = false
func start(connectivity: ConnectivityObserving) async {
await connectivity.statusStream
.map { !$0.isConnected }
.assign(to: \.isOffline, on: self)
}
}- Getting Started
- Connectivity · App Lifecycle · Keyboard
- Appearance · Locale · Protected Data · Notification Permission
- Async Streams
ForgeObservers is part of the Forge family of Swift packages for iOS.
| Package | Description |
|---|---|
| ForgeCore | Thread-safe primitives for iOS Swift packages. |
| ForgeInject | Dependency injection with constructor and property wrapper support. |
| ForgeObservers | Reactive system observers — connectivity, lifecycle, keyboard, and more. |
| ForgeStorage | Type-safe key-value, file, and Keychain storage. |
| ForgeOrchestrator | Orchestrate app flows — startup gates, data pipelines, and continuous monitors. |
| ForgePush | Push notification management — permissions, tokens, and routing. |
| ForgeLocation | Location triggers — geofencing, significant changes, and visits. |
| ForgeBackgroundTasks | Background task scheduling and dispatch. |
ForgeObservers is released under the MIT License. See LICENSE.