SwiftUI Navigation popping back when modifying list binding property in a pushed view
Asked Answered
P

3

12

When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.

Xcode 13.3 beta, iOS 15.

I created a simple demo and code is below.

Shopping Lists List Edit List section Edit
ShoppingListsView ShoppingListEditView ShoppingListEditsectionView

Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.

I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.

GIF

Example

Code:

Models:

struct ShoppingList {
    let id: String = UUID().uuidString
    var title: String
    var sections: [ShoppingListSection]
}

struct ShoppingListSection {
    let id: String = UUID().uuidString
    var title: String
}

View Model:

final class ShoppingListsViewModel: ObservableObject {
    @Published var shoppingLists: [ShoppingList] = [
        .init(
            title: "Shopping List 01",
            sections: [
                .init(title: "Fresh food")
            ]
        )
    ]
}

Content View:

struct ContentView: View {
    var body: some View {
        NavigationView {
            ShoppingListsView()
        }
    }
}

ShoppingListsView

struct ShoppingListsView: View {
    @StateObject private var viewModel = ShoppingListsViewModel()

    var body: some View {
        List($viewModel.shoppingLists, id: \.id) { $shoppingList in
            NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
                Text(shoppingList.title)
            }
        }
        .navigationBarTitle("Shopping Lists")
    }
}

ShoppingListEditView

struct ShoppingListEditView: View {
    @Binding var shoppingList: ShoppingList

    var body: some View {
        Form {
            Section(header: Text("Title")) {
                TextField("Title", text: $shoppingList.title)
            }
            Section(header: Text("Sections")) {
                List($shoppingList.sections, id: \.id) { $section in
                    NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
                        Text(section.title)
                    }
                }
            }
        }
        .navigationBarTitle("Edit list")
    }
}

ShoppingListSectionEditView

struct ShoppingListSectionEditView: View {
    @Binding var section: ShoppingListSection

    var body: some View {
        Form {
            Section(header: Text("Title")) {
                TextField("title", text: $section.title)
            }
        }
        .navigationBarTitle("Edit section")
    }
}

Poyang answered 11/9, 2021 at 12:25 Comment(0)
C
24

try this, works for me:

struct ContentView: View {
    var body: some View {
        NavigationView {
            ShoppingListsView()
        }.navigationViewStyle(.stack)  // <--- here
    }
}
Corsair answered 11/9, 2021 at 12:45 Comment(7)
Works great! Although very strange... Do you know why this is the case sorry? I'm assuming the default .automatic is columns on iPhone, and that's causing the issue here. I tried iPad split view with a .columns navigation style and it didn't have any navigation issues (though my primary column wasn't updated when the published property was)Poyang
glad it worked for you as well. Sorry, don't know why this works, even after reading the so helpful docs from Apple.Corsair
Searched the internet for hours until I found this. This answer needs way more attention.Blinking
This was half my problem. I fixed the other contributing issue when I noticed the popping only happened to the views created inside a ForEach. Rather than using UUID() as my object's id, I changed it to another property, a PK, which fixed it.Horowitz
Didn't work for me. Also tried isDetailLink(false) in the NavigationLink, didn't work either. iOS 15.Retinoscope
Didn't work for me either.. iOS 15.4Causeuse
This was a Life Saver, I ran into the same issue after passing one Binding into several nested NavigationLinks.Millenarian
M
2

Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.

Detail view seems will pop when object id changed.

Monocycle answered 2/5, 2022 at 8:17 Comment(0)
D
1

The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the @StateObject.

Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the @StateObject.

The solution to this is to change the @StateObject to @EnvironmentObject

Please refactor your code to change ShoppingListsViewModel to use an @EnvironmentObject wrapper instead of a @StateObject wrapper

You may pass the environment object in to all your child views and also add a boolean @Published flag to track any updates to the data.

Then your ShoppingListView would look as below

struct ShoppingListsView: View {
    @EnvironmentObject var viewModel = ShoppingListsViewModel()

    var body: some View {
        List($viewModel.shoppingLists, id: \.id) { $shoppingList in
            NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
                Text(shoppingList.title)
            }
        }
        .navigationBarTitle("Shopping Lists")
    }
}

Don't forget to pass the viewModel in to all your child views.

That should fix your problem.

Diecious answered 31/12, 2022 at 17:28 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.