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
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.
// Register the delegate in AppDelegate
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
} // 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.