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

3

6

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()
    }

}

Buncombe answered 26/6, 2021 at 17:21 Comment(0)
C
8

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()
}
Carte answered 29/6, 2021 at 9:4 Comment(1)
Thank you! I am guessing this is probably the recommended way of doing this because it seems elegant. I wish Apple would provide more SwiftUI examples showing best practices for these types of things.Buncombe
A
0

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
      }
  }

Alfeus answered 19/9, 2023 at 22:56 Comment(0)
U
-1

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

Univalve answered 26/6, 2021 at 18:27 Comment(1)
I got that part. However, if I put "@EnvironmentObject var viewRouter: ViewRouter" in the AppDelegate, XCode throws an error "Unknown attribute 'EnvironmentObject'". So, the question is how do I call the method that I want to call if I cant instantiate it in the AppDelegate?Buncombe

© 2022 - 2024 — McMap. All rights reserved.