Home > other >  SwiftUI view crashes when updating NavigationLinks in same NavigationView
SwiftUI view crashes when updating NavigationLinks in same NavigationView

Time:01-10

To explain this behavior, I have built a simplified project that creates same behavior as my working project. Please keep in mind that my real project is much bigger and have more constraints than this one.

I have a simple struct with only an Int as property (named MyObject) and a model that stores 6 MyObject.

struct MyObject: Identifiable {
    let id: Int
}

final class Model: ObservableObject {
    let objects: [MyObject] = [
        MyObject(id: 1),
        MyObject(id: 2),
        MyObject(id: 3),
        MyObject(id: 4),
        MyObject(id: 5),
        MyObject(id: 6)
    ]
}

Please note that in my real project, Model is more complicated (loading from JSON).

Then I have a FavoritesManager that can add and remove a MyObject as favorite. It has a @Published array of ids (Int):

final class FavoritesManager: ObservableObject {
    @Published var favoritesIds = [Int]()
    
    func addToFavorites(object: MyObject) {
        guard !favoritesIds.contains(object.id) else { return }
        favoritesIds.append(object.id)
    }
    
    func removeFromFavorites(object: MyObject) {
        if let index = favoritesIds.firstIndex(of: object.id) {
            favoritesIds.remove(at: index)
        }
    }
}

My App struct creates Model and FavoritesManager and injects them in my first view:

@main
struct testAppApp: App {
    @StateObject private var model = Model()
    @StateObject private var favoritesManager = FavoritesManager()    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(model)
                .environmentObject(favoritesManager)
        }
    }
}

Now I have few views that display my objects:

ContentView displays a link to ObjectsListView using NavigationLink and a FavoritesView. In my real project, ObjectsListView does not display all objects, only a selection (by category for example).

struct ContentView: View {
    @EnvironmentObject var model: Model
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(
                    destination: ObjectsListView(objects: model.objects), label: {
                        Text("List of objects")
                    }
                )
                FavoritesView()
                    .padding()
            }
        }
    }
}

FavoritesView displays ids of current selected favorites. It observes FavoritesManager to update its body when a favorite is added/removed.

Important: it creates a new NavigationLink to favorites details view. If you remove this link, bug does not happen.

struct FavoritesView: View {
    @EnvironmentObject var model: Model
    @EnvironmentObject var favoritesManager: FavoritesManager
    var body: some View {
        HStack {
            Text("Favorites:")
            let favoriteObjects = model.objects.filter( { favoritesManager.favoritesIds.contains($0.id) })
            ForEach(favoriteObjects) { object in
                NavigationLink(destination: DetailsView(object: object), label: {
                    Text("\(object.id)")
                })
            }
        }
    }
}

ObjectsListView simply displays objects and makes a link to DetailsView.

struct ObjectsListView: View {
    @State var objects: [MyObject]
    var body: some View {
        List {
            ForEach(objects) { object in
                NavigationLink(destination: DetailsView(object: object), label: { Text("Object \(object.id)")})
            }
        }
    }
}

DetailsView displays object details and add a button to add/remove current object as favorite. If you press the star, current object will be added as favorite (as expected) and FavoritesView will be updated to display/remove this new id (as expected).

But here is the bug: as soon as you press the star, current view (DetailsView) disappears and it goes back (without animation) to previous view (ObjectsListView).

struct DetailsView: View {
    @EnvironmentObject var favoritesManager: FavoritesManager
    @State var object: MyObject
    var body: some View {
        VStack {
            HStack {
                Text("You're on object \(object.id) detail page")
                
                Button(action: {
                    if favoritesManager.favoritesIds.contains(object.id) {
                        favoritesManager.removeFromFavorites(object: object)
                    },
                    else {
                        favoritesManager.addToFavorites(object: object)
                    }
                }, label: {
                    Image(systemName: favoritesManager.favoritesIds.contains(object.id) ? "star.fill" : "star")
                        .foregroundColor(.yellow)
                })
            }
            
            Text("Bug is here: if your press the star, it will go back to previous view.")
                .font(.caption)
                .padding()
        }
    }
}

You can download complete example project here.

You may ask why I don't display objects list in ContentView directly. It is because of my real project. It displays objects classified by categories in lists. I have reproduce this behavior with ObjectsListView.

I also need to separated Model and FavoritesManager because in my real project I don't want that my ContentView's body is updated everytime a favorite is added/removed. That's why I have moved all favorites displaying in FavoritesView.

I believe that this issue happens because I add/remove NavigationLinks in current NavigationView. And it looks like it is reloading whole navigation hierarchy. But I can't figure out how to get expected result (staying in DetailsView when pressing star button) with same views hierarchy. I may do something wrong somewhere...

CodePudding user response:

Add .navigationViewStyle(.stack) to your NavigationView in ContentView. With that, your code works well for me, I can press the star in DetailsView, and it remains displayed. It does not go back to the previous View. Using macos 12.2, Xcode 13.2, targets ios 15.2 and macCatalyst 12.1. Tested on real devices.

  •  Tags:  
  • Related