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.