FI
ForgePush

ForgeVisiblePush

Route notification responses — taps and custom actions — to registered handlers via UNUserNotificationCenterDelegate.

VisiblePushHandler

Conform to this protocol to handle a specific type of visible push response. Implement matches(_:) to claim the response and handle(_:context:) to act on it. Each handler has a unique id used for logging and deduplication.

import ForgeVisiblePush
import UserNotifications

protocol VisiblePushHandler: Sendable {
    /// Unique identifier for this handler. Used for logging and deduplication.
    var id: String { get }

    /// Return true if this handler should process the given notification response.
    func matches(_ response: UNNotificationResponse) -> Bool

    /// Handle the notification response (e.g., navigate, update state).
    func handle(_ response: UNNotificationResponse, context: VisiblePushContext) async
}

VisiblePushContext

Runtime context passed to handle(_:context:) — provides connectivity status and whether protected data is accessible. Same shape as SilentPushContext.

// Runtime context passed to handle(_:context:)
struct VisiblePushContext: Sendable {
    /// Current network connectivity status.
    let connectivity: ConnectivityStatus

    /// Whether protected data (Keychain, CoreData) is accessible.
    let protectedDataAvailable: Bool
}

Implementing a Handler

TaskTappedHandler.swift
import ForgeVisiblePush
import UserNotifications

struct TaskTappedHandler: VisiblePushHandler {
    let id = "task-tapped"

    func matches(_ response: UNNotificationResponse) -> Bool {
        let userInfo = response.notification.request.content.userInfo
        return userInfo["type"] as? String == "task_reminder"
    }

    func handle(_ response: UNNotificationResponse, context: VisiblePushContext) async {
        let userInfo = response.notification.request.content.userInfo
        guard let taskId = userInfo["task_id"] as? String else { return }
        await MainActor.run {
            TaskFlowNavigator.shared.navigate(to: .task(id: taskId))
        }
    }
}

VisiblePushRouter

Initialize with connectivity and protected data observers. Add handlers with addHandler(_:). When a notification response arrives, the router calls matches(_:) on each handler and dispatches all matching handlers concurrently via TaskGroup.

import ForgeVisiblePush

let router = VisiblePushRouter(
    connectivity: connectivityObserver,
    protectedData: protectedDataObserver
)
router.addHandler(TaskTappedHandler())
router.addHandler(ProjectUpdateTappedHandler())

Removing Handlers

// Remove a handler by its id
router.removeHandler("task-tapped")

Delegate Integration

Implement UNUserNotificationCenterDelegate — typically in AppDelegate — and forward responses to the router. The router provides both a completion-handler variant and an async variant.

AppDelegate.swift
// Register the delegate in AppDelegate
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    UNUserNotificationCenter.current().delegate = self
    return true
}
AppDelegate+UNDelegate.swift
// UNUserNotificationCenterDelegate

// Option 1: Completion handler variant
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    visibleRouter.handleResponse(response, completionHandler: completionHandler)
}

// Option 2: Async variant
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    Task {
        await visibleRouter.handleResponse(response)
        completionHandler()
    }
}

// Optional: control foreground presentation
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
    completionHandler([.banner, .sound, .badge])
}

Note: VisiblePushRouter dispatches to all matching handlers concurrently (same behavior as SilentPushRouter). If a notification matches multiple handlers, they all run in parallel.