Make a notification tap navigate somewhere specific by storing a URL in the notification's
userInfo, opening it from the notification delegate, and handling it with onOpenURL. One URL
pipeline then serves links from Safari and from notifications alike.
Handle the URL in SwiftUI
Register a URL scheme in the target's URL types settings first, then parse incoming URLs with
onOpenURL. Test by opening myapp://offer in Safari.
ContentView()
.onOpenURL { url in
guard let host = url.host(), host == "offer" else { return }
offerShown = true
}
.sheet(isPresented: $offerShown) {
OfferView()
}
Put the URL in the Notification
Store the deep link as a string in userInfo when scheduling.
extension UNUserNotificationCenter {
func addOfferNotification() {
let content = UNMutableNotificationContent()
content.title = String(localized: "offerTitle")
content.body = String(localized: "offerBody")
content.userInfo = ["url": "myapp://offer"]
let request = UNNotificationRequest(
identifier: "offer",
content: content,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 1800, repeats: false)
)
add(request)
}
}
Open It From the Delegate
The notification center delegate must be set in willFinishLaunchingWithOptions, which means a
real AppDelegate, bridged into SwiftUI with @UIApplicationDelegateAdaptor. didReceive fires
on tap; read the URL and open it, ignoring notifications without one.
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
guard
let urlString = response.notification.request.content.userInfo["url"] as? String,
let url = URL(string: urlString)
else { return }
await UIApplication.shared.open(url)
}
}
Wire It Into the App
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var delegate
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup { ContentView() /* onOpenURL as above */ }
.onChange(of: scenePhase) {
if scenePhase == .background {
UNUserNotificationCenter.current().addOfferNotification()
}
}
}
}
UIApplication.shared.open routes the tap through the same onOpenURL handler as an external
link, so navigation logic lives in exactly one place.