Programmatically tapping NavigationLink on push notification

I have a very simple app displaying a list and each row is a NavigationLink to the detail view. This is a simplified version with what I think are the most important parts:

class Element: ObservableObject {
  @Published var id: String
  ...
}

class ListViewModel: ObservableObject {
  @Published var elements: [Element]
  ...
}

struct ContentView: View {
  @ObservedObject private var viewModel: ListViewModel
  @EnvironmentObject private var pushHandler: PushNotificationHandler

  var body: some View {
    List(viewModel.elements) {
      NavigationLink(destination: DetailView(element: element)) {
        ...
      }
    }
  }
}

Now the app receives push notifications referring to each of the elements by id, i.e. the userInfo dictionary contains one of the ids belonging to an Element. What I would like to do now is navigate directly to the DetailView when the user taps on the push notification.

I also have a class listening to notification center delegate calls that looks something like this:

class PushNotificationHandler: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
  @Published var lastNotificationResponseID: String?

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

So far I have discovered that NavigationLink can be created with isActive, which is a Binding<Bool> and which when set to .constant(true) pushes the DetailView as soon as I start the app.

My problem is - how do I pipe the lastNotificationResponseID into the NavigationLink's isActive and compare it to element.id of the corresponding row so that the correct DetailView is pushed when the push is tapped?

Okay, I might be onto something, I've discovered another way to create a NavigationLink, namely:

NavigationLink(destination: DetailView(element: element), tag: element.id, selection: $pushHandler.lastNotificationResponseID) {
  ...
}

This indeed works when I tap the body of a push notification, i.e. the detail view is displayed. However a strange thing happens when I try to display the detail view by manually tapping on a NavigationLink - all the rows in the list disappear :thinking:

Could you show us the code with programmatic link? If all the links disappear when tapping on the notification - maybe the viewModel.elements somehow get unset/not initialized?

Unfortunately I am not at liberty to share my complete code, but I was able to create a minimal app does reproduces the same problem. Its code is here: https://github.com/janagrill/nav-link-test

The problem is for some reason that I have @EnvironmentObject var localPushHandler: LocalPushHandler in two views, namely RootView and ContentView. As soon as I comment out this line in the RootView the cells no longer disappear, but I have no idea why :woman_shrugging:

I managed to reproduce the issue. I am looking at EnvironmentObject wrapper documentation and it says:

An environment object invalidates the current view whenever the observable object changes. If you declare a property as an environment object, be sure to set a corresponding model object on an ancestor view by calling its environmentObject(_:) modifier.

So I think what's happening is that when a view has reference to any environment object and it gets changed it has to redraw the view. Whenever you set the selectedSong inside the ContentView, its parent (the RootView) has to be redrawn because it holds the reference to localPushHandler. And that's why, when you comment out that line in RootView, the issue disappears (unlike rows). No reference - no need to redraw when the object changes.

1 Like

I saw the same documentation comment, but I don't think this explains the issue I'm seeing. Yes, the RootView gets redrawn if the EnvironmentObject changes, but then its children, including ContentView should also get redrawn once and there should be no flickering or disappearing. To me this looks like some kind of a bug.

Given the RootView is redrawn it recreates the ContentView.

Putting a print statement inside the dateFormatter initialiser of ContentView prints the date formatter's address. It's different each time you go inside (setting song to something) the ContentView and when going out of it (setting song to nothing).

You can also add the

handleEvents(receiveOutput: { _ in print("Loaded data") })

in operator chain inside ViewModel. You'll see that it prints each time going out/in of the DetailsView. So when RootView redraws it recreates the ContentView which in turn recreates the dateformatter as well as re-requests the data for the list.

1 Like

Alright, I understand now what you mean :+1: Thank you for your help! :slight_smile:

1 Like

Happy to help! :grinning_face_with_smiling_eyes:

1 Like
Terms of Service

Privacy Policy

Cookie Policy