2

I'm trying to build out a simple navigation where you can click on items in a link and pop back to the root controller from a sheet view. As you can see from the video below, when I tap on an item in the list, the wrong item is loaded (there's an offset between the row I click and the one that gets highlighted and loaded).

I also get the error SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.

Here's all my code:

import SwiftUI

struct ContentView: View {
    @State var rootIsActive:Bool = false
    
    var body: some View {
        NavigationView{
            AllProjectView(rootIsActive: self.rootIsActive)
        }
        .navigationBarTitle("Root")
        .navigationViewStyle(StackNavigationViewStyle())
        .environment(\.rootPresentationMode, self.$rootIsActive)
    }
}

struct AllProjectView: View {
    @State var rootIsActive:Bool = false
    @State var projects: [String] = ["1", "2", "3"]
    
    var body: some View{
        List{
            ForEach(projects.indices, id: \.self){ idx in
                ProjectItem(name: self.$projects[idx], rootIsActive: self.$rootIsActive)
            }
        }.navigationBarTitle("All Projects")
    }
}


struct ProjectItem: View{
    @Binding var name: String
    @Binding var rootIsActive: Bool
    
    init(name: Binding<String>, rootIsActive: Binding<Bool>){
        self._name = name
        self._rootIsActive = rootIsActive
    }
    
    var body: some View{
        NavigationLink(
            destination: ProjectView(name: self.name),
            isActive: self.$rootIsActive){
            Text(name)
        }
        .isDetailLink(false)
        .padding()
    }
}

struct ProjectView: View {
    var name: String
    @State var isShowingSheet: Bool = false
    @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
    @Environment(\.rootPresentationMode) private var rootPresentationMode: Binding<RootPresentationMode>
    
    var body: some View{
        VStack{
            Text(name)
            Button("Show Sheet"){
                self.isShowingSheet = true
            }
        }
        .sheet(isPresented: $isShowingSheet){
            Button("return to root"){
                self.isShowingSheet = false
                print("pop view")
                self.presentationMode.wrappedValue.dismiss()
                print("pop root")
                self.rootPresentationMode.wrappedValue.dismiss()
            }
        }
        .navigationBarTitle("Project View")
    }
}


// from https://stackoverflow.com/a/61926030/1720985

struct RootPresentationModeKey: EnvironmentKey {
    static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}

extension EnvironmentValues {
    var rootPresentationMode: Binding<RootPresentationMode> {
        get { return self[RootPresentationModeKey.self] }
        set { self[RootPresentationModeKey.self] = newValue }
    }
}

typealias RootPresentationMode = Bool

extension RootPresentationMode {
    
    public mutating func dismiss() {
        self.toggle()
    }
}

enter image description here

scientiffic
  • 9,045
  • 18
  • 76
  • 149
  • `ForEach` should have an explicit id it or make `ProjectItem` identifiable. Actually `ForEach` can't differentiate between items otherwise if you don't give them a seperate id – gtxtreme Sep 14 '21 at 19:29
  • Got it - I added an id to the ForEach but still run into the same issue. Will update the code in the post. – scientiffic Sep 14 '21 at 19:43

1 Answers1

0

You only have one isRootActive variable that you're using. And, it's getting repeated for each item on the list. So, as soon as any item on the list is tapped, the isActive property for each NavigationLink turns to true.

Beyond that, your isRootActive isn't actually doing anything right now, since your "Return to root" button already does this:

self.isShowingSheet = false
self.presentationMode.wrappedValue.dismiss()

At that point, there's nothing more to dismiss -- it's already back at the root view.

My removing all of the root and isActive stuff, you get this:


struct ContentView: View {
    var body: some View {
        NavigationView{
            AllProjectView()
        }
        .navigationBarTitle("Root")
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

struct AllProjectView: View {
    @State var projects: [String] = ["1", "2", "3"]
    
    var body: some View{
        List{
            ForEach(projects.indices, id: \.self){ idx in
                ProjectItem(name: self.$projects[idx])
            }
        }.navigationBarTitle("All Projects")
    }
}


struct ProjectItem: View{
    @Binding var name: String
    
    var body: some View{
        NavigationLink(
            destination: ProjectView(name: self.name)
        ){
            Text(name)
        }
        .isDetailLink(false)
        .padding()
    }
}

struct ProjectView: View {
    var name: String
    @State var isShowingSheet: Bool = false
    @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
    
    var body: some View{
        VStack{
            Text(name)
            Button("Show Sheet"){
                self.isShowingSheet = true
            }
        }
        .sheet(isPresented: $isShowingSheet){
            Button("return to root"){
                self.isShowingSheet = false
                print("pop view")
                self.presentationMode.wrappedValue.dismiss()
            }
        }
        .navigationBarTitle("Project View")
    }
}

If you had an additional view in the stack, you would need a way to keep track of if the root were active. I've used a custom binding here that converts an optional String representing the project's name to a Bool value that gets passed down the view hierarchy:

struct ContentView: View {
    var body: some View {
        NavigationView{
            AllProjectView()
        }
        .navigationBarTitle("Root")
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

struct AllProjectView: View {
    @State var projects: [String] = ["1", "2", "3"]
    @State var activeProject : String?
    
    func activeBindingForProject(name : String) -> Binding<Bool> {
        .init {
            name == activeProject
        } set: { newValue in
            activeProject = newValue ? name : nil
        }
    }
    
    var body: some View{
        List{
            ForEach(projects.indices, id: \.self){ idx in
                InterimProjectView(name: self.$projects[idx],
                                   isActive: activeBindingForProject(name: self.projects[idx]))
            }
        }.navigationBarTitle("All Projects")
    }
}

struct InterimProjectView: View {
    @Binding var name : String
    @Binding var isActive : Bool
    
    var body : some View {
        NavigationLink(destination: ProjectItem(name: $name, isActive: $isActive),
                       isActive: $isActive) {
            Text("Next : \(isActive ? "true" : "false")")
        }
    }
}

struct ProjectItem: View {
    @Binding var name: String
    @Binding var isActive: Bool
    
    var body: some View{
        NavigationLink(
            destination: ProjectView(name: self.name, isActive: $isActive)
        ){
            Text(name)
        }
        .isDetailLink(false)
        .padding()
    }
}

struct ProjectView: View {
    var name: String
    @Binding var isActive : Bool
    @State var isShowingSheet: Bool = false
    
    var body: some View{
        VStack{
            Text(name)
            Button("Show Sheet"){
                self.isShowingSheet = true
            }
        }
        .sheet(isPresented: $isShowingSheet){
            Button("return to root"){
                self.isShowingSheet = false
                print("pop root")
                self.isActive.toggle()
            }
        }
        .navigationBarTitle("Project View")
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94