push-notificationswiftuiapple-push-notifications

SwiftUI - Open a specific View when user opens a Push Notification


A have an app made in SwiftUI, with Parse used for DB. I'm some parts of the app i've integrated some cloud functions that send notifications (for example: when someone send's you a message, you will receive a push notification triggered by that cloud function). In the past days i'm struggling and searching for how to open a specific view when you press the Notification to open the app. I've found some solutions, but could not make them work.

This is the code that i have so far :

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        
        //Parse Intialization
        ...
        //notifications
        registerForPushNotifications()
        //Notification Badge
        UIApplication.shared.applicationIconBadgeNumber = 0
        // start notification while app is in Foreground
        UNUserNotificationCenter.current().delegate = self
        return true
    }
    
    // This function will be called right after user tap on the notification
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print("app opened from PushNotification tap")
        UIApplication.shared.applicationIconBadgeNumber = 0
      completionHandler()
    }
}

@main
struct MyApp: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView(currentTab: Tab.home)
        }
    }
}

The app prints "app opened from PushNotification tap", but if I put a variable in AppDelegate and I listen for changes in ContentView with .onReceive or .onChange for that variable, nothing is hapenning

struct ContentView: View {
    @ObservedObject var appState = AppState()
    @State var currentTab : Tab
    @State var noReloadAddItemView = false
    var body: some View {
        TabView(selection: $appState.currentTab) {
            
            NavigationView {
                HomeView(appState: appState, noReloadAddItemView: $noReloadAddItemView)
                
            }
            .tabItem {
                if appState.currentTab == .home {
                    Image(systemName: "house.fill")
                } else {
                    Image(systemName: "house")
                }
                
                Text(LocalizedStringKey("HomeTabMenu"))
                
            }.tag(Tab.home)
            
            NavigationView {
                SearchView(appState: appState, noReloadAddItemView: $noReloadAddItemView)
            }
            .tabItem {
                if appState.currentTab == .search {
                    Image(systemName: "magnifyingglass.circle.fill")
                } else {
                    Image(systemName: "magnifyingglass")
                }
                Text(LocalizedStringKey("SearchTabMenu"))
            }.tag(Tab.search)
            
            NavigationView {
                AddItemView(appState: appState, noReloadAddItemView: $noReloadAddItemView)
            }
            .tabItem {
                if appState.currentTab == .add {
                    Image(systemName: "plus.circle.fill")
                } else {
                    Image(systemName: "plus.circle")
                }
                Text(LocalizedStringKey("SellTabMenu"))
            }.tag(Tab.add)
            
            NavigationView {
                ShoppingCartFavoritesView(appState: appState, noReloadAddItemView: $noReloadAddItemView)
            }
            .tabItem {
                if appState.currentTab == .favorites {
                    Image(systemName: "cart.fill")
                } else {
                    Image(systemName: "cart")
                }
                Text(LocalizedStringKey("CartTabMenu"))
            }.tag(Tab.favorites)
            
            NavigationView {
                ProfileView(appState: appState, noReloadAddItemView: $noReloadAddItemView)
            }
            .tabItem {
                if appState.currentTab == .profile {
                    Image(systemName: "person.fill")
                } else {
                    Image(systemName: "person")
                }
                Text(LocalizedStringKey("ProfileTabMenu"))
            }.tag(Tab.profile)
        }
        .accentColor(Color("ColorMainDark"))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(currentTab: Tab.home)
    }
}

class AppState: ObservableObject {
    @Published var currentTab : Tab = .home
}

enum Tab {
    case home, search, add, favorites, profile
}

Solution

  • You need some sort of shared state that you can modify that SwiftUI knows to react to. An ObservableObject is perfect for this:

    class AppState: ObservableObject {
        static let shared = AppState()
        @Published var pageToNavigationTo : String?
    }
    

    Then, to listen to it and respond to it, you can do a couple different methods in your main view.

    Option 1 -- NavigationLink binding based on the value of the ObservedObject:

    struct ContentView : View {
        @ObservedObject var appState = AppState.shared //<-- note this
        @State var navigate = false
        
        var pushNavigationBinding : Binding<Bool> {
            .init { () -> Bool in
                appState.pageToNavigationTo != nil
            } set: { (newValue) in
                if !newValue { appState.pageToNavigationTo = nil }
            }
        }
        
        var body: some View {
            NavigationView {
                Text("My content")
                    .overlay(NavigationLink(destination: Dest(message: appState.pageToNavigationTo ?? ""),
                                            isActive: pushNavigationBinding) {
                        EmptyView()
                    })
            }
        }
    }
    
    struct Dest : View {
        var message : String
        var body: some View {
            Text("\(message)")
        }
    }
    

    Or, you could use onReceive:

    struct ContentView : View {
        @ObservedObject var appState = AppState.shared
        @State var navigate = false
        
        var body: some View {
            NavigationView {
                VStack {
                    if navigate {
                        //deprecated since 16.0: use NavigationLink(value:label:), or navigationDestination(isPresented:destination:), inside a NavigationStack or NavigationSplitView
                        NavigationLink(destination: Text("Test"), isActive: $navigate ) {
                            EmptyView()
                        }
                    }
                    Text("My content")
                        .onReceive(appState.$pageToNavigationTo) { (nav) in
                            if nav != nil { navigate = true }
                        }
                }
            }
        }
    }
    

    I'll leave the implementation details of your specific NavigationView, NavigationLink, TabView, etc to you, but this should get you started.

    Finally, a fully-functional minimal example that mocks a notification and shows how the navigation view:

    
    class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
         
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                print("Dispatch")
                AppState.shared.pageToNavigationTo = "test"
            }
            
            return true
        }
    }
    
    class AppState: ObservableObject {
        static let shared = AppState()
        @Published var pageToNavigationTo : String?
    }
    
    @main
    struct MultiWindowApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    struct ContentView : View {
        @ObservedObject var appState = AppState.shared
        @State var navigate = false
        
        var pushNavigationBinding : Binding<Bool> {
            .init { () -> Bool in
                appState.pageToNavigationTo != nil
            } set: { (newValue) in
                if !newValue { appState.pageToNavigationTo = nil }
            }
        }
        
        var body: some View {
            NavigationView {
                Text("My content")
                    .overlay(NavigationLink(destination: Dest(message: appState.pageToNavigationTo ?? ""),
                                            isActive: pushNavigationBinding) {
                        EmptyView()
                    })
            }
        }
    }
    
    
    struct Dest : View {
        var message : String
        var body: some View {
            Text("\(message)")
        }
    }