I have the following LocationToggleable class, which I have attempted to make an observable object, as well as a struct, and various other attempts. This object is being maintained by a LocationService
class, which is also an ObservableObject
and it's being published in an [LocationToggleable]
array. I understand that updating the state for an individual element of that array, doesn't necessarily publish the updates from that element, because the array itself wasn't actually updated, merely a property of an element of the array. How do I properly update a list, when a property is changed, on a @Published
array?
class LocationToggleable: Hashable, Identifiable {
// Omitted unrelated properties
var selected: Bool = false
}
class LocationToggleable: ObservableObject, Hashable, Identifiable {
// Omitted unrelated properties
@Published var selected: Bool = false
}
struct LocationToggleable: Hashable, Identifiable {
// Omitted unrelated properties
var selected: Bool = false
}
class LocationService: ObservableObject {
//Omitted unrelated properties/functions
@Published var toggleableLocations: [LocationToggleable]
}
class LocationService: ObservableObject {
//Omitted unrelated properties/functions
@StateObject var toggleableLocations: [LocationToggleable]
}
class LocationService: ObservableObject {
//Omitted unrelated properties/functions
@ObservedObject var toggleableLocations: [LocationToggleable]
}
ForEach(locationService.toggleableLocations, id: \.self) { item in
if item.selected {
Section(content: {}, header: {
TrailDetailCardView(location: item.location)
.frame(maxWidth: .infinity)
.matchedGeometryEffect(id: "location-id-\(item.id)", in: namespace)
.onTapGesture {
withAnimation {
proxy.scrollTo(item.id, anchor: UnitPoint(x: 0, y: 0.01))
locationService.setSelected(for: item, to: !item.selected)
}
}
})
.id(item.id)
}
toggleableLocations
and it reeks of a code-smell. func setSelected(for toggleableLocation: LocationToggleable, to selected: Bool) {
if let currentIndex = toggleableLocations.firstIndex(where: { $0.selected }) {
toggleableLocations[currentIndex].selected = false
}
toggleableLocation.selected = selected
toggleableLocations = toggleableLocations
}
selected
state for proper management of it's layout in the LazyVGrid
. I also need the object to be Hashable
for uses elsewhere in the code.You could try this approach as shown in the example code, to ...properly update a list, when a property is changed, on a @Published array
.
The example code uses one @StateObject var locationService = LocationService()
, the source of data truth.
It also uses the $
sign in the ForEach
to allow changes to be made to the item
,
and a simple item.selected.toggle()
to toggle the selection.
struct LocationToggleable: Hashable, Identifiable {
let id = UUID() // <--- here
// Omitted unrelated properties
var selected: Bool
var location: String // <--- for testing
}
class LocationService: ObservableObject {
//Omitted unrelated properties/functions
@Published var toggleableLocations: [LocationToggleable] = [
LocationToggleable(selected: true, location: "location-1"),
LocationToggleable(selected: false, location: "location-2"),
LocationToggleable(selected: true, location: "location-3")] // <--- here for testing, else []
}
struct ContentView: View {
@StateObject var locationService = LocationService() // <--- here
var body: some View {
ForEach($locationService.toggleableLocations) { $item in // <--- here $
if item.selected {
Section(content: {}, header: {
TrailDetailCardView(location: item) // <--- here
.frame(maxWidth: .infinity)
// .matchedGeometryEffect(id: "location-id-\(item.id)", in: namespace)
.onTapGesture {
withAnimation {
// proxy.scrollTo(item.id, anchor: UnitPoint(x: 0, y: 0.01))
item.selected.toggle() // <--- here
}
}
})
.id(item.id)
}
// --- for testing
else {
Text("\(item.location) not selected").id(item.id)
.onTapGesture {item.selected.toggle() }
}
}
}
}
// for testing
struct TrailDetailCardView: View {
var location: LocationToggleable // <--- here, use @Binding if required
var body: some View {
Text(location.selected ? "true" : "false").border(.red)
}
}