FO
ForgeObservers

ProtectedDataObserver

Monitors whether iOS-protected data (Keychain, Core Data, encrypted files) is accessible.

ProtectedDataState

A simple enum representing whether protected data is currently accessible.

ProtectedDataState.swift
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.

ProtectedDataObserving.swift
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).

ProtectedData.swift
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.

BackgroundTask.swift
// 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

BackgroundSyncViewModel.swift
@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.

Test with injected dependencies
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)
}