'SwiftUI: How to do additional work when clicking a NavigationLink?

Right now I have a navigation link

NavigationView {
     NavigationLink(destination: AnotherView())) {
          Text("Click Here")
     }
}
    .navigationBarTitle("SwiftUI")

What I want to have happen is that when I click the navigation link, I want an object to instantiated and put into a database.

My question, in other words, is: how do you perform work aside from just pushing the new view when using NavigationLink?



Solution 1:[1]

For your described scenario here is the simplest way

NavigationView {
     NavigationLink("Click Here", destination: 
         AnotherView()
             .onAppear {
                 // any action having access to current view context
             })
}

Verified as still valid with Xcode 13.3 / iOS 15.4

backup

Solution 2:[2]

SwiftUI does not provide an easy way to do this, but it does provide a way. You should use a Button whose action triggers the NavigationLink.

The problem is that the code to do this is cumbersome and ugly. So I wrote a control that wraps it up to resemble a love child of Button and NavigationLink.

struct ActionNavigationLink<Destination: View, Content: View>: View {
    private let destination: Destination
    private let content: Content
    private let action: (NavigationToken) -> ()

    @State private var continueToDestination: Bool = false

    init(
        destination: Destination,
        action: @escaping (NavigationToken) -> (),
        @ViewBuilder content: () -> Content
    ) {
        self.destination = destination
        self.action = action
        self.content = content()
    }

    var body: some View {
        Button(action: buttonAction, label: { content })
            .background(
                NavigationLink(
                    destination: destination,
                    isActive: $continueToDestination,
                    label: { EmptyView() }
                )
            )
    }

    private func buttonAction() {
        let token = NavigationToken {
            self.continueToDestination = true
        }

        action(token)
    }
}

The NavigationToken is a very simple struct that takes an action. That action defines how navigation will be performed once triggered.

struct NavigationToken {
    private let navigationAction: () -> ()

    fileprivate init(_ action: @escaping () -> ()) {
        self.navigationAction = action
    }

    func navigate() {
        navigationAction()
    }
}

This token will be passed to the action you provide when using this control to give you full power over when the navigation is actually performed. So if you need to perform some API call and only navigate once the callback returns, then you can totally make that happen.

Here I'm simulating an API call by delaying the navigation by 3 seconds.

ActionNavigationLink(
    destination: EmptyView(),
    action: { token in
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
            token.navigate()
        }
    }
) {
    Text("Delayed Navigation")
}

Full source can be found on this gist.

Solution 3:[3]

There are overloads for NavigationLink that accept a Binding. You an init it with a binding and emit a side effect in the setter or similarly you can use a model with some @Published property and just subscribe to its inner publisher. Here is an example that does both:

class Model: ObservableObject {
  @Published var isLinkActive = false
  var subscriptions = Set<AnyCancellable>()
  init() {
    $isLinkActive
      .sink { _ in
        print("I'm a side effect in the model")
      }
    .store(in: &subscriptions)
  }
}

struct ContentView: View {
  @ObservedObject var model: Model
  let isLinkActive: Binding<Bool>
  init(model: Model = .init()) {
    self.model = model
    self.isLinkActive = Binding<Bool>(
      get: { [model] in model.isLinkActive},
      set: { [model] in
        print("I'm a side effect in the binding!")
        model.isLinkActive = $0
    })
  }
  var body: some View {
    NavigationView {
      NavigationLink("hello", destination: Color.red, isActive: isLinkActive)
    }
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Kris McGinnes
Solution 3 Josh Homann