Cameron Henige
Cameron Henige

Reputation: 416

How can I deep link from a notification to a screen in SwiftUI?

I am trying to set up deep linking in my app. I have set up the app as well as the push notifications. However, I am unable to complete the final link from where the AppDelegate receives the user click on a notification to the screen that I want to deep link to. I basically want to call viewRouter.goToFred() from the AppDelegate. I have set up a git repo here (https://github.com/cameronhenige/SwiftUITestDeepLink) with an app that has everything set up except for this final piece. Could somebody help me figure this out? Here are the relevant pieces of code. Thanks!



import SwiftUI

struct MainView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        NavigationView {

            List {

            ForEach(viewRouter.pets) { pet in
                NavigationLink(
                    destination: PetView(),
                    tag: pet,
                    selection: $viewRouter.selectedPet,
                    label: {
                        Text(pet.name)
                    }
                )
            }
                Button("Send Notification to Fred") {
                    viewRouter.sendNotification()
                }
                
                Button("Manually go to Fred") {
                    viewRouter.goToFred()
                }
            }
            

        }
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}



import SwiftUI

@main
struct ContentView: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            MainView().environmentObject(ViewRouter())

        }

}
    
}


import Foundation
import UserNotifications

class ViewRouter: ObservableObject {
    @Published var selectedPet: Pet? = nil
    @Published var pets: [Pet] = [Pet(name: "Louie"), Pet(name: "Fred"), Pet(name: "Stanley")]
    
    
    func goToFred() {
        self.selectedPet = pets[1]
    }
    
    func sendNotification() {
        let content = UNMutableNotificationContent()
        let categoryIdentifire = "Notification Type"
        
        content.title = "Go To Fred"
        content.body = "Click me to go to Fred."
        content.sound = UNNotificationSound.default
        content.badge = 1
        content.categoryIdentifier = categoryIdentifire
        
        let request = UNNotificationRequest(identifier: "identifier", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error {
                print("Error \(error.localizedDescription)")
            }
        }
        
    }
    
}



import SwiftUI

struct PetView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        
        if let pet = viewRouter.selectedPet {
            Text(pet.name)
        } else {
            EmptyView()
        }
    }
}

struct PetView_Previews: PreviewProvider {
    static var previews: some View {
        PetView()
    }
}



import Foundation

struct Pet: Identifiable, Hashable {
    var name: String
    var id: String { name }

}



import Foundation
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {
    
    let notificationCenter = UNUserNotificationCenter.current()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
    
        if #available(iOS 10.0, *) {
          // For iOS 10 display notification (sent via APNS)
          UNUserNotificationCenter.current().delegate = self

          let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
          UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: {_, _ in })
        } else {
          let settings: UIUserNotificationSettings =
          UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
          application.registerUserNotificationSettings(settings)
        }

        application.registerForRemoteNotifications()
        
        return true
    }

}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
      withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
      let userInfo = notification.request.content.userInfo
        print("Notification created")

      // Change this to your preferred presentation option
      completionHandler([[.alert, .sound]])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
      let userInfo = response.notification.request.content.userInfo
        
        print("user clicked on notification. Now take them to Fred.")
        //todo Somehow I want to call viewRouter.goToFred(), but I don't have access to that object in AppDelegate
        
      completionHandler()
    }

}

Upvotes: 6

Views: 7314

Answers (3)

coreyd303
coreyd303

Reputation: 87

I am using a different pattern to accomplish something very similar, that doesn't require me to expose the environment object to the app delegate. In our case instead of a router, we have an app state container object, but ultimately it works in a very similar way.

I found that this was important to do because changing the values on appState requires that those changes happen from within the context of the App. If I updated the path property on appState from within the AppDelegate it resulted in a disconnect between the value on the instance of the injected appState object and the referenced appState object provided through the .environmentObject modifier. This caused views to receive the state changes down stream, but they could not send updates back upstream.

In AppDelegate add a published property

final class AppDelegate: NSObject, UIApplicationDelegate {

    @Published var userNotificationPath: String?

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let userInfo = response.notification.request.content.userInfo
        userNotificationPath = userInfo["path"] as? String

        ...
    }
}

monitor the property from App for publications, and update the environment object appState.

struct MyApp: App {

    @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
    @StateObject var appState = AppStateContainer()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environmentObject(appState)
                .onReceive(appDelegate.$userNotificationPath) { path in
                  appState.path = path
                  appDelegate.userNotificationPath = nil
                }
        }
    }
}

monitor the appState value in any view needing to respond

struct SomeChildView: View {

  @EnvironmentObject private var appState: AppState
  
  var Body: some View {
    View()
      .onReceive(appState.$path) { path in
         ...handle business
      }
  }

Upvotes: 0

Sardorbek Ruzmatov
Sardorbek Ruzmatov

Reputation: 921

I try this approach and it works. Admittedly I dont know if this is the correct or suggested way.

The appDelegate has to get reference to the same ViewRouter instance that MainView is using, and where ContentView has to hook the appDelegate with the ViewRouter instance.

ContentView.swift:

@main
struct ContentView: App {
    
    let router = ViewRouter()
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
  
    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(router)
                .onAppear(perform:setUpAppDelegate)
        }
    }
    
    func setUpAppDelegate(){
        appDelegate.router = router
    }
}

AppDelegate.swift:

weak var router: ViewRouter?
func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
  let userInfo = response.notification.request.content.userInfo
    
    print("user clicked on notification. Now take them to Fred.")
    //todo Update the ViewRouter to
    router?.goToFred()
    
    completionHandler()
}

Upvotes: 8

Elias Al Zaghrini
Elias Al Zaghrini

Reputation: 295

It's well detailed in Apple's documentation

All you have to do is to call the method that you want to run when clicking on a notification in userNotificationCenter(_:didReceive:withCompletionHandler:) in the AppDelegate

Upvotes: -1

Related Questions