In the code below, the detail view UI does not update when the FavoriteButton is tapped. We know the binding is connected to the ObservableObject because the didSet is called and prints the updated state of the foos array. Oddly, I found that adding a VStack in the FooList's NavigationView resolves the bug in this instance, but did not for the more complicated UI where I encountered this issue.
Am I missing something about how this should be connected? Another workaround is to toggle a State bool in the FavoriteButton, but that seems like it goes against the point of Bindings.
import SwiftUI
struct Foo: Identifiable {
var id = UUID()
var name: String
var isFavorite: Bool
}
class FooData: ObservableObject {
@Published var foos = [Foo(name: "Test", isFavorite: true)] {
didSet {
print(foos)
}
}
}
struct FooList: View {
@ObservedObject var fooData = FooData()
var body: some View {
NavigationView {
// Adding a VStack makes the FavoriteButton update correctly, but this doesn't work for more complicated UIs
// VStack {
List {
ForEach(fooData.foos, id: \.id) { foo in
let index = fooData.foos.firstIndex(where: { $0.id == foo.id })!
NavigationLink(foo.name, destination: FooDetailView(foo: $fooData.foos[index]))
}
}
// }
}
}
}
struct FooDetailView: View {
@Binding var foo: Foo
var body: some View {
FavoriteButton(isFavorite: $foo.isFavorite)
}
}
struct FavoriteButton: View {
@Binding var isFavorite: Bool
var body: some View {
Button(action: {
isFavorite.toggle()
}, label: {
if isFavorite {
Image(systemName: "heart.fill")
} else {
Image(systemName: "heart")
}
})
}
}
UPDATE:
Passing FooData as an EnvironmentObject to the List and declaring the EnvironmentObject in the FavoriteButton resolves the issue. This must be sufficient to tell SwiftUI that this View cares about any updates to the FooData, even if no code references the EnvironmentObject directly. This feels a little like magic, definitely not the most intuitive behavior for me. Hopefully, this can help someone else.
Full working code:
import SwiftUI
struct Foo: Identifiable {
var id = UUID()
var name: String
var isFavorite: Bool
}
class FooData: ObservableObject {
@Published var foos = [Foo(name: "Test", isFavorite: true)] {
didSet {
print(foos)
}
}
}
struct FooList: View {
@ObservedObject var fooData = FooData()
var body: some View {
NavigationView {
List {
ForEach(fooData.foos, id: \.id) { foo in
let index = fooData.foos.firstIndex(where: { $0.id == foo.id })!
NavigationLink(foo.name, destination: FooDetailView(foo: $fooData.foos[index]))
}
}
}
.environmentObject(fooData)
}
}
struct FooDetailView: View {
@Binding var foo: Foo
var body: some View {
FavoriteButton(isFavorite: $foo.isFavorite)
}
}
struct FavoriteButton: View {
@EnvironmentObject var fooData: FooData
@Binding var isFavorite: Bool
var body: some View {
Button(action: {
isFavorite.toggle()
}, label: {
if isFavorite {
Image(systemName: "heart.fill")
} else {
Image(systemName: "heart")
}
})
}
}