I've got a List of NavigationLinks. Each NavigationLink is wrapped by a "isVisible" boolean check. Separately, a config page has Toggle's that set the visibility on or off.
The problem is when I come back from the config page, the main view doesnt update itself based on the new visibility boolean on all the items. i.e. the "if (hv.isVisible)" isn't reevaluated.
I know the booleans are being persisted. When I restart the app, the correct items in the list are shown
@ObservedObject fileprivate var viewModel = HealthValuesViewModel.instance
// in here values is defined as
// @Published var values: [HealthValue] = []
// which is then populated in init()
// The HealthValue is an ObservableObject class and has property
// @Published var isVisible: Bool
List(viewModel.values, id:\.type.rawValue) { hv in
if (hv.isVisible) {
NavigationLink(destination: hv.view) {
HealthValueCellView(healthValue: hv )
}
}
}
I've been playing with all sorts of combinations of @ObervedObject, @Published etc. Also tried making sure I have an id in the List.
Any help or suggestions greatly appreciated.
UPDATE: As suggested, I've created a reproducible example. Hope this makes things clearer
// DummyView is the main view - has a settings button and a list of 3 objects.
// DummySettingsView is the sub-view where the items in the list are toggled on/off
// DummySettingsCellView are the elements in the settings view list that have the toggle
import SwiftUI
enum ValueType: String {
case type1 = "T1"
case type2 = "T2"
case type3 = "T3"
}
class DummyValueObject : Identifiable, ObservableObject {
let type: ValueType
let symbolName: String
@Published var isVisible: Bool = true {
didSet {
print("ValueObject type=\(type.rawValue), isVisible = \(isVisible)")
}
}
init(type: ValueType, symbolName: String) {
self.type = type
self.symbolName = symbolName
}
}
class DummyViewModel : ObservableObject {
static let instance = DummyViewModel()
@Published var values: [DummyValueObject] = [
DummyValueObject(type: .type1, symbolName: "heart.fill"),
DummyValueObject(type: .type2, symbolName: "wind"),
DummyValueObject(type: .type3, symbolName: "scalemass.fill")
]
}
struct DummySettingCellView: View {
@ObservedObject var value: DummyValueObject
var body: some View {
HStack {
Image(systemName: value.symbolName)
Text(value.type.rawValue)
Toggle("", isOn: $value.isVisible)
}
}
}
struct DummySettingsView: View {
@ObservedObject var model = DummyViewModel.instance
var body: some View {
VStack {
List(model.values) { v in
DummySettingCellView(value: v)
}
}
}
}
struct DummyView: View {
@ObservedObject var model = DummyViewModel.instance
var body: some View {
VStack {
NavigationLink(destination: DummySettingsView()) {
Image(systemName: "wrench")
}
Spacer()
List(model.values, id:\.type.rawValue) { v in
if (v.isVisible) {
HStack {
Image(systemName: v.symbolName)
Text(v.type.rawValue)
}
}
}
}
}
}
I should probably have mentioned that this is a watch app - but I guess it shouldnt make a difference.
Try this approach, using a struct
for the DummyValueObject
instead of a nested ObservableObject
, and a @Binding var value: DummyValueObject
to pass it into DummySettingCellView
Also note, the important use of List(model.values.filter{$0.isVisible})
instead of using
if (v.isVisible) {...}
, to allow the view to refresh properly.
struct ContentView: View {
var body: some View {
NavigationStack { // <-- here
DummyView()
}
}
}
struct DummyView: View {
@StateObject var model = DummyViewModel() // <-- here
var body: some View {
VStack {
NavigationLink(destination: DummySettingsView(model: model)) { // <-- here
Image(systemName: "wrench")
}
Spacer()
List(model.values.filter{$0.isVisible}) { v in // <-- here important
HStack {
Image(systemName: v.symbolName)
Text(v.type.rawValue)
}
}
}
}
}
struct DummySettingsView: View {
@ObservedObject var model: DummyViewModel // <-- here
var body: some View {
VStack {
List($model.values) { $v in // <-- here
DummySettingCellView(value: $v) // <-- here
}
}
}
}
class DummyViewModel : ObservableObject {
@Published var values: [DummyValueObject] = [
DummyValueObject(type: .type1, symbolName: "heart.fill"),
DummyValueObject(type: .type2, symbolName: "wind"),
DummyValueObject(type: .type3, symbolName: "scalemass.fill")
]
}
struct DummyValueObject : Identifiable { // <-- here
let id = UUID() // <-- here
let type: ValueType
let symbolName: String
var isVisible: Bool = true {
didSet {
print("---> DummyValueObject type=\(type.rawValue), isVisible = \(isVisible)")
}
}
init(type: ValueType, symbolName: String) {
self.type = type
self.symbolName = symbolName
}
}
struct DummySettingCellView: View {
@Binding var value: DummyValueObject // <-- here
var body: some View {
HStack {
Image(systemName: value.symbolName)
Text(value.type.rawValue)
Toggle("", isOn: $value.isVisible)
Spacer()
}
}
}
enum ValueType: String {
case type1 = "T1"
case type2 = "T2"
case type3 = "T3"
}
EDIT-1
You can of course use (not recommended), your nested ObservableObjects
.
In that case, follow this approach, still using @Binding var value: DummyValueObject
in DummySettingCellView
, it will fix your current problem, but may create others
in the other parts of your code, if not now, then later on.
Note, you should not use @ObservedObject var model = DummyViewModel.instance
thinking
you can use it as a singleton in different views, this is not correct.
The @StateObject var model = DummyViewModel()
in DummyView
should be the
only source of truth for your app. Pass this model around to the views that need it.
Have a look at this link, it gives you some good official examples of how to manage data in your app Managing model data in your app
class DummyViewModel : ObservableObject {
//static let instance = DummyViewModel() <--- NOT THIS
@Published var values: [DummyValueObject] = [
DummyValueObject(type: .type1, symbolName: "heart.fill"),
DummyValueObject(type: .type2, symbolName: "wind"),
DummyValueObject(type: .type3, symbolName: "scalemass.fill")
]
}
struct DummySettingCellView: View {
@Binding var value: DummyValueObject // <-- here
var body: some View {
HStack {
Image(systemName: value.symbolName)
Text(value.type.rawValue)
Toggle("", isOn: $value.isVisible)
}
}
}
struct DummySettingsView: View {
@ObservedObject var model: DummyViewModel // <-- here
var body: some View {
VStack {
List($model.values) { $v in // <-- here $
DummySettingCellView(value: $v) // <-- here
}
}
}
}
struct DummyView: View {
@StateObject var model = DummyViewModel() // <-- here
var body: some View {
VStack {
NavigationLink(destination: DummySettingsView(model: model)) { // <-- here
Image(systemName: "wrench")
}
Spacer()
// -- here --
List(model.values.filter{$0.isVisible}, id:\.type.rawValue) { v in
HStack {
Image(systemName: v.symbolName)
Text(v.type.rawValue)
}
}
}
}
}