80

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.
aheze
  • 24,434
  • 8
  • 68
  • 125
G. Marc
  • 4,987
  • 4
  • 32
  • 49
  • 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? –  Jul 21 '19 at 08:46
  • 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. – G. Marc Jul 21 '19 at 13:03

10 Answers10

104

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.

starball
  • 20,030
  • 7
  • 43
  • 238
mohit kejriwal
  • 1,755
  • 1
  • 10
  • 19
  • Thanks - I've been looking for this for a while. – LateNate Oct 18 '19 at 12:44
  • 10
    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. – FontFamily Nov 11 '20 at 00:05
  • 1
    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 it – Kross Jun 18 '21 at 15:09
  • 1
    @Kross 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. – mohit kejriwal Jun 18 '21 at 15:55
  • 1
    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? – user2619824 Aug 08 '21 at 01:01
71

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))
ideastouch
  • 1,307
  • 11
  • 6
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.

ShadowDES
  • 783
  • 9
  • 20
  • 1
    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. – G. Marc Jul 25 '19 at 09:30
  • 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. – ShadowDES Jul 25 '19 at 16:13
  • Yes, it's working exactly like I want it, the only issue now is the look of the button. – G. Marc Jul 25 '19 at 16:36
  • In your WorkoutList, change "List" to "ForEach". That gets rid of all the >. Might not be the final solution, but might get you closer. – ShadowDES Jul 25 '19 at 17:37
  • Thanks, I believe this is should be the correct answer. – rahimli Mar 23 '22 at 21:53
20

Use the NavigationLink inside the button's label.

Button(action: {
    print("Floating Button Click")
}, label: {
    NavigationLink(destination: AddItemView()) {
         Text("Open View")
     }
})
yOshi
  • 820
  • 7
  • 12
  • 2
    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. – yOshi Feb 13 '20 at 14:47
  • small issue that row deselects after tap – malhal Apr 11 '22 at 16:57
  • Thank you soooooo much!, I was putting NavigationLink on the button, never thought of putting it inside the label! – marticztn Feb 14 '23 at 11:36
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?

Chongsheng Sun
  • 441
  • 5
  • 5
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.

6

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.

Andrew K
  • 1,571
  • 1
  • 17
  • 25
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
        }
    }
Goblin
  • 11
  • 6
0

I changed the List to ScrollView and it worked.

0

My preferred syntax, use closures for everything

NavigationLink {
    WorkoutDetail(workout: workout)
} label: {
    WorkoutRow(workout: workout)
}
.buttonStyle(ButtonStyle3D(background: Color.yellow))
byaruhaf
  • 4,128
  • 2
  • 32
  • 50