Search code examples
swiftswiftuiazure-notificationhub

How to process deep link from an aps notification using the same @EnvironmentObject used in onOpenUrl without using a singleton in AppDelegate


I am trying to coordinate my deep link with push notifications so they both process my custom url scheme in the same manner and navigate to the appropriate view. The challenge seems to be with push notifications and how to process the link passed, through an apn from Azure Notification Hubs, using the same @EnvironmentObject that the onOpenUrl uses without breaking the SwiftUI paradigm and using a singleton.

Here is how I trigger the notification on my simulator, which works fine and navigates me to the appropriate view:

xcrun simctl openurl booted "myapp://user/details/123456"

Which triggers this the onOpenUrl in this code:

var body: some Scene {
    WindowGroup {
        ContentView()
            .environmentObject(sessionInfo)
            .onOpenURL { url in
                print("onOpenURL: \(url)")
                if sessionInfo.processDeepLink(url: url) {
                    print("deep link TRUE")
                } else {
                    print("deep link FALSE")
                }
            }
    }
}

And all my DeepLinks work just as desired. I wanted to trigger them from a notification so I created an apns file with the same link that worked using xcrun:

{
  "aps": {
    "alert": { // alert data },
    "badge": 1,
    "link_url":"myapp://user/details/123456"
  }
}

and pushed it to the simulator like this:

xcrun simctl push xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx com.myco.myapp test.apn

How do I reference my object from the AppDelegate which gets the message:

func notificationHub(_ notificationHub: MSNotificationHub!, didReceivePushNotification message: MSNotificationHubMessage!) {
    
    print("notificationHub...")
    
    let userInfo = ["message": message!]
    print("user: ")
    NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
    
    if (UIApplication.shared.applicationState == .background) {
        print("Notification received in the background")
    } else {
        print("Notification received in the foreground")
    }
    
    UIApplication.shared.applicationIconBadgeNumber = 4
}

I looked at this post, but couldn't relate the components to my app, possibly due to the NotificationHub part of it. I also saw this post, but again didn't know how to connect it.

I saw this post and it looks like push notification and deep linking are two different things. I could use the same logic if I could access the SessionInfo object from the AppDelegate. I'm concerned about messing around in there given I'm new to iOS development. Here is what I'm using in the App:

@StateObject var sessionInfo: SessionInfo = SessionInfo()

This post seems to cover it but I'm still having trouble. I don't know what this means:

static let shared = AppState()

And my SessionInfo is decorated with @MainActor. When I access it from other places I use:

@EnvironmentObject var sessionInfo: SessionInfo

I also saw this post, which there was no selected answer, but the one which did exist seemed to recommend making the AppDelegate and EnvrionmentObject and push it into the ContentView. I think what I really need is the AppDelegate when the notification arrives to update something shared/published to the SessionInfo object so the url is parsed and the navigation kicked off. This seems backwards to me.

This post makes the AppDelegate an ObservableObject with a property which is published and makes the AppDelegate an EnvrionmentObject, so when the value is updated, it's published. If it were the navigation link/object that would work but something would still need to process it and it would not make sense for the onOpenUrl to use the AppDelegate, so again I think this is backwards.

If I did follow the post where there is a static SessionInfo object in the SessionInfo class, singleton, that means I would need to remove the @EnvironmentObject var sessionInfo: SessionInfo from the ContentView and the .environmentObject(sessionInfo) on the main View I am using I think and instead instantiate the shared object in each view where it is used. Right? It seems like I followed this whole @EnvrionmentObject, @StateObject, @MainActor paradigm and would have to abandon it. I'm not sure if that is right or what the tradeoffs are.

Most recently this post seems to be pretty in-depth, but introduces a new element, UNUserNotificationCenter, which I heard referenced in this youtube video.


Solution

  • This article was very helpful for the notification part.

    Azure NotificationHubs the message info is in message.userInfo["aps"] vs userInfo["aps"] in the example or most places I have seen it. Not much documentation on MSNotificationHubMessage:

    func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification message: MSNotificationHubMessage) {
        
        print("notificationHub...")
        
        let title = message.title ?? ""
        let body = message.body ?? ""
        print("title: \(title)")
        print("body: \(body)")
        
        let userInfo = ["message": message]
    
        NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
     
        guard let aps = message.userInfo["aps"] as? [String: AnyObject] else {
            return
        }
    
    ...
    }
    

    Second, this post provided the answer which I adapted for my project:

    final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MSNotificationHubDelegate {
    
        var navMgr: NavigationManager = NavigationManager()
    
       ...
    }
    

    and

    @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(sessionInfo)
                .environmentObject(appDelegate.navMgr)
                .onOpenURL { url in
                    print("onOpenURL: \(url)")
                    if sessionInfo.processDeepLink(url: url) {
                        print("deep link TRUE")
                    } else {
                        print("deep link FALSE")
                    }
                }
        }
    }