Search code examples
swiftuiswiftui-list

List view not hiding/unhiding elements based on a state variable


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.


Solution

  • 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)
                    }
                }
            }
        }
    }