In a SwiftUI app, in order to communicate with the AppDelegate
, you need to use UIApplicationDelegateAdaptor
. With it, you will be able to pass your AppDelegate
to your views using the environmentObject
modifier.
In your AppDelegate
's userNotificationCenter(_:didReceive:)
method, you will need to extract info from your notification in order to create an object that will be used to navigate to a specific view in your app.
Using Combine, you will then be able to publish this object and observe it in your root view in order to drive your navigation.
The following iOS 17 implementation shows how to navigate to a specific view in a SwiftUI app when a notification is received and tapped by the user.
AppDelegate.swift:
import UIKit
import Combine
final class AppDelegate: NSObject, ObservableObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
let newOfferSubject = PassthroughSubject<Offer, Never>()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
if let offer = Offer(rawValue: response.notification.request.content.categoryIdentifier) {
newOfferSubject.send(offer)
}
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
return [.banner, .list]
}
}
MyDemoApp.swift:
import SwiftUI
@main
struct MyDemoApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appDelegate)
}
}
}
Offer.swift:
enum Offer: String, Identifiable {
case special = "offer.special"
case tenPercent = "offer.ten_percent"
var id: Self {
self
}
}
ContentView.swift:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var appDelegate: AppDelegate
@State var offer: Offer?
var body: some View {
List {
Button("Request authorization") {
Task {
await requestAuthorization()
}
}
Button(#"Send "Special offer" notification"#) {
sendOfferNotification(withIdentifier: "offer.special")
}
Button(#"Send "10% offer" notification"#) {
sendOfferNotification(withIdentifier: "offer.ten_percent")
}
}
.onReceive(appDelegate.newOfferSubject) { newOffer in
self.offer = newOffer
}
.sheet(item: $offer) { offer in
switch offer {
case .special:
Text("Special offer")
case .tenPercent:
Text("10% offer")
}
}
}
}
extension ContentView {
private func requestAuthorization() async {
do {
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound])
print("Request authorization for notifications granted: \(granted).")
} catch {
print("Error while requesting authorization for notifications: \(error).")
}
}
private func sendOfferNotification(withIdentifier identifier: String) {
let content = UNMutableNotificationContent()
content.title = "New offer"
content.body = "We have an offer for you."
content.categoryIdentifier = identifier
let notificationRequest = UNNotificationRequest(identifier: "OfferIdentifier", content: content, trigger: nil)
UNUserNotificationCenter.current().add(notificationRequest, withCompletionHandler: nil)
}
}
userNotificationCenter(_ center: UNUserNotificationCenter, didReceive...
is called while application is in background, so no UI is present then, and you just need to store somewhere notification response data to show user later, when user opens an app, but then app just behaves as in regular flow (eg. read records from database, defaults, etc.) – Golly