Search code examples
swiftuiswiftui-listswiftui-navigationlinkios-navigationviewswiftui-foreach

SwiftUI Navigation Controller stuttering with two Navigationlinks per List row


I am trying to create two NavigationLinks in a repeating List. Each has a separate destination. The code all works fine until I imbed the call to the root view in a List/ForEach loop. At which point the navigation becomes very strange.

Try to click on either link and then click the back indicator at the top. It will go to one NavigationLink, and then the other. Sometimes in a different order, and sometimes it will auto-return from one of the links, and othertimes it won't open the second detail view until you return from the first detail view. It does this both in Preview, as well as if you build and run the application.

I have distilled down the code to the most basic below. If you comment the 2 lines as indicated in ContentView, you will then see correct behavior.

I am running Catalina 10.15.5, xCode 11.6, with the application target of IOS 13.6.

How can I modify the code, so that it will work with the List/ForEach loop?

import SwiftUI

struct DetailView1: View {
    var body: some View {
        HStack {
            Text("Here is Detail View 1." )}
            .foregroundColor(.green)
    }
}

struct DetailView2: View {
    var body: some View {
        HStack {
            Text( "Here is Detail View 2.") }
            .foregroundColor(.red)
    }
}

struct RootView: View {
    var body: some View {
        HStack {
            VStack {
                NavigationLink(destination: DetailView1())
                { VStack { Image(systemName: "ant.circle").resizable()
                    .frame(width:75, height:75)
                    .scaledToFit()
                }.buttonStyle(PlainButtonStyle())
                    Text("Tap for Detail 1.")
                        .foregroundColor(.green)
                }
            }
            NavigationLink(destination: DetailView2())
            { Text("Tap for Detail 2.")
                .foregroundColor(.red)
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            // Comment the following line for correct behavior
            List { ForEach(0..<3) {_ in
                RootView()
                // Comment the following line for correct behavior
                }  }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            ContentView()
                .navigationBarTitle("Strange Behavior")
        }
    }
}

Solution

  • In your case both navigation links are activated at once user tap a row, to avoid this below is possible approach

    Tested with Xcode 12 / iOS 14

    The idea is to have one link which is activated programmatically and destination is selected dynamically depending on which button is clicked

    struct RootView: View {
        @State private var isFirst = false
        @State private var isActive = false
        var body: some View {
            HStack {
                VStack {
                    Button(action: {
                        self.isFirst = true
                        self.isActive = true
                    })
                    { VStack { Image(systemName: "ant.circle").resizable()
                        .frame(width:75, height:75)
                        .scaledToFit()
                    }
                        Text("Tap for Detail 1.")
                            .foregroundColor(.green)
                    }
                }
                Button(action: {
                    self.isFirst = false
                    self.isActive = true
                })
                { Text("Tap for Detail 2.")
                    .foregroundColor(.red)
                }
                .background(
                    NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
                )
            }
            .buttonStyle(PlainButtonStyle())
        }
    
        @ViewBuilder
        private func destination() -> some View {
            if isFirst {
                DetailView1()
            } else {
                DetailView2()
            }
        }
    }