30

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
  • 34,912
  • 15
  • 99
  • 151
Jnguyen22
  • 707
  • 1
  • 7
  • 11
  • 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

51

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!")
                })
Nguyễn Khắc Hào
  • 1,370
  • 11
  • 22
  • 2
    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
  • 12
    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
  • 11
    Yes, if you follow this technique within a List it will not work. – Mahmud Ahsan Mar 25 '20 at 04:10
7

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
  • 71
  • 1
  • 1
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)

4

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
  • 4,881
  • 1
  • 29
  • 33
4

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
  • 370
  • 1
  • 13
0

I also just used:

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

iOS 15.3 deployment target.

Radu
  • 2,042
  • 2
  • 20
  • 39