53

I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.

ZStack {
    NavigationLink(destination: TradeView(trade: trade)) {
        TradeButton()
    }
    Button(action: {
        print("Hello world!") //this is the only thing that runs
    }) {
        TradeButton()
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Jnguyen22
  • 967
  • 2
  • 8
  • 13
  • One way (there may be others) is to run this function from the `onAppear` modifier for `TradeView`. Two limitations.... the sequence and `onAppear` may happen only once. I use `onAppear` to check for a prerequisite in a `sheet`. It won't *prevent* the view (in your case, `TradeView` from appearing, but it will execute a function. –  Aug 27 '19 at 01:47
  • Possible duplicate of [How to do something(for example: print("hi")) in NavigationLink before moving to a destinationView](https://stackoverflow.com/questions/56962928/how-to-do-somethingfor-example-printhi-in-navigationlink-before-moving-to) – kontiki Aug 27 '19 at 05:00

6 Answers6

83

You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:

NavigationLink(destination: TradeView(trade: trade)) {
    Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
    print("Hello world!")
})
Arturo
  • 3,254
  • 2
  • 22
  • 61
Nguyễn Khắc Hào
  • 1,980
  • 2
  • 15
  • 25
  • 3
    Fantastic solution! I've used "onAppear()" as proposed by many others before, but it caused all sorts of issues. But this is simple and clean. Thanks so much. – G. Marc Feb 15 '20 at 13:24
  • Is there are way when the user clicks the "back" button to return to the previous view that it can pass back a variable? Or even force the refresh of the previous view? – Learn2Code Feb 20 '20 at 03:36
  • 20
    This does not seem to work with you have a list of items and they are in the NavigationLink parent!? Has anybody else had any luck with this? – Learn2Code Feb 29 '20 at 21:26
  • 19
    Yes, if you follow this technique within a List it will not work. – Mahmud Ahsan Mar 25 '20 at 04:10
13

You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.

struct Sidebar: View {
    @State var isTapped = false

    var body: some View {
        NavigationLink(destination: ViewToPresent(),
                       isActive: Binding<Bool>(get: { isTapped },
                                               set: { isTapped = $0; print("Tapped") }),
                       label: { Text("Link") })
    }
}

struct ViewToPresent: View {
    var body: some View {
        print("View Presented")
        return Text("View Presented")
    }
}

The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:

Tapped
Tapped
View Presented
Tapped
sramhall
  • 131
  • 1
  • 2
8

NavigationLink + isActive + onChange(of:)

// part 1
@State private var isPushed = false

// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
    Text("")
})

// part 3
.onChange(of: isPushed) { (newValue) in
    if newValue {
        // do what you want
    }
}
hstdt
  • 5,652
  • 2
  • 34
  • 34
  • I don't know why this is accepted as an answer. This is working as expected and not depending on any gesture thing. – arango_86 Dec 13 '22 at 15:57
7

This works for me atm:

@State private var isActive = false

NavigationLink(destination: MyView(), isActive: $isActive) {
    Button {
        // run your code
        
        // then set
        isActive = true

    } label: {
        Text("My Link")
    }
}
toad
  • 400
  • 2
  • 13
4

Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.

struct ContentView: View {
    @EnvironmentObject var navigationModel: NavigationModel

    var body: some View {
        NavigationView {
            List(0 ..< 10, id: \.self) { row in
                NavigationLink(destination: DetailView(id: row),
                               tag: row,
                               selection: self.$navigationModel.linkSelection) {
                    Text("Link \(row)")
                }
            }
        }
    }
}

struct DetailView: View {
    var id: Int;

    var body: some View {
       Text("DetailView\(id)")
    }
}

class NavigationModel: ObservableObject {
    @Published var linkSelection: Int? = nil {
        didSet {
            if let linkSelection = linkSelection {
                // action
                print("selected: \(String(describing: linkSelection))")
            }
        }
    }
}

It this example you need to pass in your model to ContentView as an environment object:

ContentView().environmentObject(NavigationModel())

in the SceneDelegate and SwiftUI Previews.

The model conforms to ObservableObject protocol and the property must have a @Published attribute.

(it works within a List)

ganczar
  • 51
  • 2
-4

I also just used:

NavigationLink(destination: View()....) { 
    Text("Demo")
}.task { do your stuff here }

iOS 15.3 deployment target.

Radu
  • 2,076
  • 2
  • 20
  • 40
  • 2
    This is not a solution, `.task` is not meant for this, in fact if you place that in a `ForEach` that has the `NavigationLink` it will repeat the action several times. – Arturo Jun 16 '22 at 01:20
  • this creates infinite loop, not recommended – Trevor Feb 21 '23 at 13:35