ProtectedDataObserver
Monitors whether iOS-protected data (Keychain, Core Data, encrypted files) is accessible.
ProtectedDataState
A simple enum representing whether protected data is currently accessible.
public enum ProtectedDataState: Sendable {
case available
case unavailable
} ProtectedDataObserving Protocol
The protocol provides the current state, a stream, and a convenience method to suspend until protected data becomes available.
public protocol ProtectedDataObserving: Sendable {
/// The current protected data state (synchronous read)
var state: ProtectedDataState { get }
/// Stream of protected data availability changes
var stateStream: AsyncStream<ProtectedDataState> { get }
/// Suspends until protected data becomes available.
/// Returns immediately if already available.
func waitUntilAvailable() async
} @MainActor Init
ProtectedDataObserver has a @MainActor init because it reads UIApplication.shared.isProtectedDataAvailable during initialization. This means you must create it on the main actor -- either from a @MainActor context or using ForgeInject's mainActorBuilder.
Usage
Subscribe to stateStream to react when protected data availability changes (e.g., when the device is locked or unlocked).
import ForgeObservers
// ProtectedDataObserver requires @MainActor for init
// because it reads UIApplication.shared.isProtectedDataAvailable
@MainActor
func setupProtectedData() {
let protectedData = ProtectedDataObserver()
Task {
for await state in protectedData.stateStream {
switch state {
case .available:
print("Protected data available")
case .unavailable:
print("Protected data locked")
}
}
}
} Waiting for Availability
The waitUntilAvailable() method is essential for background tasks. It suspends until the device is unlocked and protected data becomes accessible, then returns immediately. If protected data is already available, it returns without suspending.
// In a background task, wait for protected data before
// accessing Keychain, Core Data, or encrypted files.
func performBackgroundWork() async {
let protectedData: ProtectedDataObserving = // resolve from DI
// Suspends until the device is unlocked.
// Returns immediately if already available.
await protectedData.waitUntilAvailable()
// Now safe to access Keychain, encrypted SQLite, etc.
let credentials = try await Keychain.read("api_token")
await syncWithServer(token: credentials)
} ViewModel Example
@Observable
final class BackgroundSyncViewModel {
private let protectedData: ProtectedDataObserving
var isProtectedDataAvailable = false
init(protectedData: ProtectedDataObserving) {
self.protectedData = protectedData
}
func startObserving() async {
for await state in protectedData.stateStream {
isProtectedDataAvailable = state == .available
}
}
} Testing
ProtectedDataObserver accepts both an injectable NotificationCenter and an explicit initialState — the latter lets tests construct the observer without touching UIApplication.shared.isProtectedDataAvailable, which is useful when running outside a real app context.
import UIKit
import ForgeObservers
@Test @MainActor
func protectedDataObserverReactsToLock() async throws {
// Inject both the NotificationCenter and an explicit initial state —
// avoids touching UIApplication.shared in the test environment.
let center = NotificationCenter()
let observer = ProtectedDataObserver(
notificationCenter: center,
initialState: .available
)
center.post(name: UIApplication.protectedDataWillBecomeUnavailableNotification, object: nil)
try await Task.sleep(for: .milliseconds(50))
#expect(observer.state == .unavailable)
}