FO
ForgeObservers

NotificationPermissionObserver

Tracks push notification permission status. Re-checks on every app foreground event and streams changes in real time.

NotificationPermissionStatus

An enum representing all possible push notification authorization states.

NotificationPermissionStatus.swift
public enum NotificationPermissionStatus: Sendable {
    case notDetermined
    case denied
    case authorized
    case provisional
    case ephemeral
}

NotificationPermissionObserving Protocol

The protocol provides the current status and a stream. Since iOS provides no system callback when the user changes notification permission in Settings, the observer re-checks the status on every app foreground event.

NotificationPermissionObserving.swift
public protocol NotificationPermissionObserving: Sendable {
    /// The current permission status (synchronous read)
    var status: NotificationPermissionStatus { get }

    /// Stream of permission status changes. Emits the current value immediately
    /// on subscription, then re-checks on every app foreground event since iOS
    /// provides no callback when the user changes permission in Settings.
    var statusStream: AsyncStream<NotificationPermissionStatus> { get }

    /// Forces a fresh check against UNUserNotificationCenter.
    /// Called automatically on app foreground.
    func refresh() async
}

Helper Extensions

The protocol includes convenience computed properties for the most common permission states.

NotificationPermissionObserving+Helpers.swift
// Convenience extensions on NotificationPermissionObserving
extension NotificationPermissionObserving {
    /// true when status is .authorized, .provisional, or .ephemeral
    var isGranted: Bool { get }

    /// true when status is .denied
    var isDenied: Bool { get }

    /// true when status is .notDetermined
    var canRequestPermission: Bool { get }
}

// Note: For requesting permission, use PushPermission from ForgePush.

Note — For requesting permission, use PushPermission from ForgePush. NotificationPermissionObserver is observation-only.

Usage

NotificationPermission.swift
import ForgeObservers

let notifications = NotificationPermissionObserver()

// Check current status
print("Status: \(notifications.status)")
print("Granted: \(notifications.isGranted)")
print("Can request: \(notifications.canRequestPermission)")

// Subscribe to changes
Task {
    for await status in notifications.statusStream {
        print("Permission status: \(status)")
    }
}

Reacting to Changes

The stream emits the current value immediately on subscription, then re-emits each time the status changes. Use this to drive UI that reflects the current permission state without polling.

ObservingChanges.swift
// React to permission changes as they happen.
// The stream emits the current value immediately on subscription,
// then re-emits whenever the status changes (e.g. after returning from Settings).
func observePermissionChanges() async {
    let notifications: NotificationPermissionObserving = // resolve from DI

    for await status in notifications.statusStream {
        switch status {
        case .authorized, .provisional, .ephemeral:
            enableNotificationFeatures()
        case .denied:
            showPermissionDeniedBanner()
        case .notDetermined:
            break // Permission not yet requested — use ForgePush's PushPermission to request
        }
    }
}

ViewModel Example

Bind notification permission state to a ViewModel for use in settings screens.

TaskFlowSettingsViewModel.swift
@Observable
final class TaskFlowSettingsViewModel {
    private let notifications: NotificationPermissionObserving

    var isGranted = false
    var isDenied = false
    var canRequest = false

    init(notifications: NotificationPermissionObserving) {
        self.notifications = notifications
        // Sync initial state synchronously before stream starts
        isGranted = notifications.isGranted
        isDenied = notifications.isDenied
        canRequest = notifications.canRequestPermission
    }

    func startObserving() async {
        for await status in notifications.statusStream {
            isGranted = notifications.isGranted
            isDenied = notifications.isDenied
            canRequest = notifications.canRequestPermission
        }
    }

    /// Force a re-check (e.g. after returning from a deep link to Settings).
    func refresh() async {
        await notifications.refresh()
    }
}