13

I'm trying to do a NavigationLink within a List or ForEach Loop in SwiftUI. Unfortunately I get a really weird behavior (e.g. when clicking on Leo it opens Karl, Opening Max points to Karl, too).

I've already figured out that it's related to the "isActive" attribute in the NavigationLink. Unfortunately, I need it to achieve a this behavior here: https://i.stack.imgur.com/g0BFz.gif which is also asked here SwiftUI - Nested NavigationView: Go back to root.

I also tried to work with selection and tag attribute but I wasn't able to achieve the "go back to root" mechanics.

Here's the Example:


import SwiftUI


struct Model: Equatable, Hashable {
    var userId: String
    var firstName: String
    var lastName: String
}


struct ContentView: View {
    
    @State var navigationViewIsActive: Bool = false
    
    var myModelArray: [Model] = [
        Model(userId: "27e880a9-54c5-4da1-afff-05b4584b1d2f", firstName: "Leo", lastName: "Test"),
        Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Max", lastName: "Test"),
        Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Karl", lastName: "Test")]
    
    var body: some View {
        NavigationView {
            List(myModelArray, id: \.self) { model in
                NavigationLink(destination: secondView(firstName: model.firstName), isActive: $navigationViewIsActive){ Text(model.firstName) }
            }
            .listStyle(PlainListStyle())
        }
    }
}

struct secondView: View {
    
    @State var firstName: String
    
    var body: some View {
        NavigationView {
            Text(firstName)
                .padding()
        }
    }
    
}

Thanks!

SwiftUIRookie
  • 591
  • 7
  • 16

2 Answers2

18

This happened because of the using of only one state navigationViewIsActive

So when you click in a navigation link , the value will change to True , and all the links will be active

The solution for this scenario is like that :

  • Define a new State which will hold the selected model value
  • You need just one NavigationLink , and make it Hidden (put it inside a VStack)
  • In the List use Button instead of NavigationLink
  • When a Button is clicked : first change the selectedModel value , than make the navigationLink active (true)

Like the code below (Tested with IOS 14) :

import SwiftUI


struct Model: Equatable, Hashable {
    var userId: String
    var firstName: String
    var lastName: String
}


struct ContentView: View {
    
    @State var navigationViewIsActive: Bool = false
    @State var selectedModel : Model? = nil
    
    var myModelArray: [Model] = [
        Model(userId: "27e880a9-54c5-4da1-afff-05b4584b1d2f", firstName: "Leo", lastName: "Test"),
        Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Max", lastName: "Test"),
        Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Karl", lastName: "Test")]
    
    var body: some View {
        NavigationView {
            VStack {
                VStack {
                    if selectedModel != nil {
                        NavigationLink(destination: SecondView(firstName: selectedModel!.firstName), isActive: $navigationViewIsActive){ EmptyView() }
                    }
                }.hidden()
                
                List(myModelArray, id: \.self) { model in
                    Button(action: {
                        self.selectedModel = model
                        self.navigationViewIsActive = true
                    }, label: {
                        Text(model.firstName)
                    })
                }
                .listStyle(PlainListStyle())
            }
            
            
        }
    }
}

struct SecondView: View {
    
    @State var firstName: String
    
    var body: some View {
        NavigationView {
            Text(firstName)
                .padding()
        }
    }
    
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

PS : I wrote this : How to navigate with SwiftUI , it will help you to understand the ways to navigate in swiftUI

Ouail Bellal
  • 1,554
  • 13
  • 27
  • This fixes a bug in iOS 14.5 - Thanks – KoCMoHaBTa Apr 29 '21 at 14:13
  • Didn't work for me. Instead it crashed! I'm assuming you meant to pass the "navigationViewIsActive" binding into the second view and then set to false from the second view. – Michael Vescovo Jun 21 '21 at 09:29
  • There's also the bound selection option but I couldn't get that to work properly either. For some reason it runs very slow and in any case I could not get it to close the view after it's been opened. "init(destination: Destination, tag: V, selection: Binding, label: () -> Label) where V : Hashable" – Michael Vescovo Jun 21 '21 at 09:37
  • Thanks - great answer. Might also add that in my case, it helped to wrap the trigger point: `self.navigationViewIsActive = true` explicitly inside a `withAnimation` block as, sometimes, the incoming view failed to animate (obviously some bug). – Konstantinos Kontos Feb 24 '22 at 05:51
  • Hello, can you show the specific animation related code? I have the same problem myself, the first trigger does not show the animation, but now it is a little unclear how to modify it better.Thanks @KonstantinosKontos – IDTIMW Apr 25 '22 at 04:24
  • This works, but there seems to be no animation on the very first click on the list item. After the first click, all items have animation – J. Edgell Apr 30 '22 at 03:05
2

You don't need isActive in this case, just use

List(myModelArray, id: \.self) { model in
    NavigationLink(destination: secondView(firstName: model.firstName)) { 
       Text(model.firstName) 
    }
}

and you have not use NavigationView in second view in this, ie.

struct secondView: View {
    
    var firstName: String   // you don't need state here as well
    
    var body: some View {
        Text(firstName)
            .padding()
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • But if I don't use isActive in this case how am I supposed to go back to the initial List from my second or third view? As from what I have researched the proper way is to use isActive and then set it to false from the third view so that the navigation flow collapses. – SwiftUIRookie Dec 21 '20 at 21:53
  • There is nothing about this wanted behavior in your question. – Asperi Dec 22 '20 at 04:49
  • Hmm sorry, didn't write it precisely enough. I wanted to express this requirement with this statement in my question "I need it to achieve a this behavior here: https://i.stack.imgur.com/g0BFz.gif ". Any ideas if this is possible? – SwiftUIRookie Dec 22 '20 at 09:48