How to show NavigationLink as a button in SwiftUI
Asked Answered
D

11

86

I've read a lot here about navigation in SwiftUI and tried a couple of things, but nothing is working as desired.

Basically, I have a view with a list of workouts and you can show a single workout by clicking on a row. This works as expected using NavigationView together with NavigationLink.

Now, I want a button on the detail view to start the workout. This should open a view with a timer. The view should be presented with the same animation as the detail view did and also show the name of the workout in the navigation bar with a back button.

I could implement this with a NavigationLink view on the details page, but the link always appears as a full width row with the arrow on the right side. I'd like this to be a button instead, but the NavigationLink seems to be resistant against styling.

struct WorkoutDetail: View {
    var workout: Workout

    var body: some View {
        VStack {
            NavigationLink(destination: TimerView()) {
                Text("Starten")
            }.navigationBarTitle(Text(workout.title))
        }
    }
}

struct WorkoutList: View {
    var workoutCollection: WorkoutCollection

    var body: some View {
        NavigationView {
            List(workoutCollection.workouts) { workout in
                NavigationLink(destination: WorkoutDetail(workout: workout)) {
                    WorkoutRow(workout: workout)
                }
            }.navigationBarTitle(Text("Workouts"))
        }
    }
}

Updated: Here's a screenshot to illustrate what I mean:

Current layout: 'Starten' with default right arrow. Desired layout: Blue button with yellow background.
Doherty answered 21/7, 2019 at 6:32 Comment(2)
How is workout being populated? Is there a model (usually some form of @ObjectBinding) behind everything? Put another way, how are you gaining your app's workout state?Kile
I've updated the post to show the list view to show how the workout is populated. The workoutCollection is loaded and handed over to the list view in the SceneDelegate class.Doherty
M
107

You don't need to wrap your view inside the NavigationLink to make it trigger the navigation when pressed.

We can bind a property with our NavigationLink and whenever we change that property our navigation will trigger irrespective of what action is performed. For example:

struct SwiftUI: View {
    @State private var action: Int? = 0
    
    var body: some View {
        
        NavigationView {
            VStack {
                NavigationLink(destination: Text("Destination_1"), tag: 1, selection: $action) {
                    EmptyView()
                }
                NavigationLink(destination: Text("Destination_2"), tag: 2, selection: $action) {
                    EmptyView()
                }
                
                Text("Your Custom View 1")
                    .onTapGesture {
                        //perform some tasks if needed before opening Destination view
                        self.action = 1
                    }
                Text("Your Custom View 2")
                    .onTapGesture {
                        //perform some tasks if needed before opening Destination view
                        self.action = 2
                    }
            }
        }
    }
}

Whenever you change the value of your Bindable property (i.e. action) NavigationLink will compare the pre-defined value of its tag with the binded property action, if both are equal navigation takes place.

Hence you can create your views any way you want and can trigger navigation from any view regardless of any action, just play with the property binded with NavigationLink.

Melpomene answered 7/9, 2019 at 19:51 Comment(5)
Thanks - I've been looking for this for a while.Tankard
This is an interesting solution to a problem we shouldn't have in Swift. When I implement this in Swift 5.2, I get the occasional error: SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.Ashleyashli
Using tags is a risky solution since there could be another component where you can get mixed behaviour or different approach, not really recomend this, instead creat a wrapper which allows you to work with a specific ID that nobody outside can replicate itMadel
@Madel not understanding the risk here. Have 2 complex applications on app-store using this approach and have never faced any mixed behaviour anywhere. The tags are created within the component and attached with the NavigationLinks of that same Component hence its not accessible from any other Component. So, outside components can freely replicate the tags and attach with their NavigationLinks, it will have no impact on the current Component's behaviour and navigation. If this is not the risk you have mentioned then please post your solution, it will help to understand better.Melpomene
How do you do this on a list that's generated dynamically like using ForEach(self.list, id: \.postId) { item in? How do I avoid the hardcoded actions and tags?Believe
M
77

I think that the accurate way to do it is using buttonStyle, for example

NavigationLink(destination: WorkoutDetail(workout: workout)) {
  WorkoutRow(workout: workout)
}
.buttonStyle(ButtonStyle3D(background: Color.yellow))
Mafaldamafeking answered 19/1, 2020 at 2:56 Comment(6)
This is exactly what I was looking for. Didn't realize you could add button styles to Navigationlinks. Wow! Going to simplify so much code!Dionysiac
Agreed. This is now the best answer as of SwiftUI 2Clasping
Best. We can implement your custom styles for buttons so it's simplest and cleanest solution as for me.Garnishee
Is this still working for SwiftUI 3? I still get the ">" at the far right, the button itself is styled though.Doherty
This should be the selected answer, since it is probably the correct & idiomatic way to do this in 2022.Napiform
Please show where you're getting ButtonStyle3D.Dasi
H
25

I've been playing around with this for a few days myself. I think this is what you're looking for.

struct WorkoutDetail: View {
var workout: Workout

var body: some View {
    NavigationView {
       VStack {
           NavigationLink(destination: TimerView()) {
               ButtonView()
           }.navigationBarTitle(Text(workout.title))
        }
    }
}

And create a View you want to show in the NavigationLink

struct ButtonView: View {
var body: some View {
    Text("Starten")
        .frame(width: 200, height: 100, alignment: .center)
        .background(Color.yellow)
        .foregroundColor(Color.red)
}

Note: Anything above the NavigationView appears to show up on all pages in the Navigation, making the size of the NavigationView Smaller in the links.

Hence answered 24/7, 2019 at 17:35 Comment(5)
Thanks, but unfortunately this isn't exactly my problem. I've updated my question with a screenshot. The thing is that I get a row for the link with the text left aligned and an arrow right aligned. But what I want is a normal button.Doherty
Not sure of the answer yet, but now I'll also look for the solution. The code above works fine, just like you want, if you are starting with a view and going to a new view (2 views total). If you are starting from a List View with a navigation link, and go to the workoutDetail view and then the timer view, it puts in the > (3 views total). Try a test project with the above code and you'll see what I mean.Hence
Yes, it's working exactly like I want it, the only issue now is the look of the button.Doherty
In your WorkoutList, change "List" to "ForEach". That gets rid of all the >. Might not be the final solution, but might get you closer.Hence
Thanks, I believe this is should be the correct answer.Charterhouse
H
22

Use the NavigationLink inside the button's label.

Button(action: {
    print("Floating Button Click")
}, label: {
    NavigationLink(destination: AddItemView()) {
         Text("Open View")
     }
})
Headspring answered 18/11, 2019 at 15:3 Comment(4)
rbaldwin I tried it in several cases, I even put more components inside and it works normally. Maybe you could upload that part of your code so I can help you.Headspring
small issue that row deselects after tapTracietracing
Thank you soooooo much!, I was putting NavigationLink on the button, never thought of putting it inside the label!Selfdevotion
Tried this, absolutely worked. but because i styled the button to be quite wide across the screen, i add a horizontal padding to the text. worked just fine. thanks yoshi!!Walkon
K
9

Very similar with Pankaj Bhalala's solution but removed ZStack:

struct ContentView: View {
    @State var selectedTag: String?

    var body: some View {
        Button(action: {
            self.selectedTag = "xx"
        }, label: {
            Image(systemName: "plus")
        })
        .background(
            NavigationLink(
                destination: Text("XX"),
                tag: "xx",
                selection: $selectedTag,
                label: { EmptyView() }
            )
        )
    }
}

Wait for a more concise solution without state. How about you?

Kellyekellyn answered 11/7, 2020 at 19:5 Comment(0)
L
6

You can use NavigationLink as Button with ZStack:

@State var tag:Int? = nil
ZStack {
  NavigationLink(destination: MyModal(), tag: 1, selection: $tag) {
    EmptyView()
  }

  Button(action: {
    self.tag = 1
  }, label: {
    Text("show view tag 1")
  })
}

Hope it will help.

Labia answered 18/5, 2020 at 11:13 Comment(0)
P
5

I don't know why all these answers are making this so complicated. In SwiftUI 2.0, you just add the button inside the navigation link!

NavigationLink(destination: TimerView()) {
    Text("Starten")
}

You can apply SwiftUI styling to the Text object just as you would style any other element.

Postexilian answered 28/11, 2020 at 0:32 Comment(3)
Because it was a SwiftUI 1.0 question.Khorma
because you will get the accessory arrow doing using your method, even in swiftui 2.0Skirret
This won't work if you need a button to perform other actions besides pushing onto the navigation stack.Calamus
C
1

You can add the navigation link directly to the button like this:

    @State var tag:Int? = nil

    ...

    NavigationLink(destination: Text("Full List"), tag: 1, selection: $tag) {
        MyButton(buttonText: "Full list") {
            self.tag = 1
        }
    }
Cowardice answered 31/8, 2020 at 17:8 Comment(0)
V
1

Been researching this for a while. This is what worked for me:

NavigationLink(destination: {
    TimerView() // your view that you want to navigate to
}, label: {
    Text("Starten") // your label/text on the button
})
.buttonStyle(.borderedProminent) // <- important line

So basically I have a normal NavigationLink, and I just apply my regular buttonStyle to it. :)

Ventura answered 15/2 at 10:15 Comment(0)
W
0

I changed the List to ScrollView and it worked.

Weissman answered 21/2, 2022 at 9:4 Comment(0)
A
0

My preferred syntax, use closures for everything

NavigationLink {
    WorkoutDetail(workout: workout)
} label: {
    WorkoutRow(workout: workout)
}
.buttonStyle(ButtonStyle3D(background: Color.yellow))
Acariasis answered 23/2, 2023 at 17:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.