11

I have a dictionary that contains various values I want to "filter" by. So I'm doing something like this

struct ExampleView : View {

    @EnvironmentObject var externalData : ExternalData

    var body: some View {
        VStack {
            ForEach(externalData.filters) { (v : (String, Bool)) in
                Toggle(isOn: $externalData.filters[v.0], label: {
                    Text("\(v.0)")
                })
            }
        }
    }
}

final class ExternalData : BindableObject {

    let didChange = PassthroughSubject<ExternalData, Never>()

    init() {
        filters["Juniper"] = true
        filters["Beans"] = false
    }

    var filters : Dictionary<String, Bool> = [:] {
        didSet {
            didChange.send(self)
        }
    }
}

This question seems related, but putting dynamic didn't seem to help and I wasn't able to figure out how to do that NSObject inheritance thing in this case. Right now, this code as is gives me this error:

Cannot subscript a value of type 'Binding<[String : Bool]>' with an argument of type 'String'

But trying to move the $ around or use paren in various ways doesn't seem to help. How can I bind the toggles to the values in my dictionary? I could just make manual toggles for each value, but that makes fragile code since (among other reasons) the potential filter values are based on a dataset that might have new values at some point.

I'm aware that I should really sort the keys (in some way) before iterating over them so the ordering is consistent, but that clutters this example so I left that code out.

Phylliida
  • 4,217
  • 3
  • 22
  • 34

1 Answers1

19

I managed to make is work by using a custom binding for each filter.

final class ExternalData: BindableObject {
    let didChange = PassthroughSubject<Void, Never>()

    var filters: Dictionary<String, Bool> = [:] {
        didSet {
            didChange.send(())
        }
    }

    init() {
        filters["Juniper"] = true
        filters["Beans"] = false
    }

    var keys: [String] {
        return Array(filters.keys)
    }

    func binding(for key: String) -> Binding<Bool> {
        return Binding(getValue: {
            return self.filters[key] ?? false
        }, setValue: {
            self.filters[key] = $0
        })
    }
}

The keys property list the filters keys as String so that it can be displayed (using ForEach(externalData.keys))

The binding(for:) method, create a custom Binding for the given key. This binding is given to the Toggle to read/write the current value in the wrapped dictionary.

The view code:

struct ExampleView : View {

    @EnvironmentObject var externalData : ExternalData

    var body: some View {
        VStack {
            ForEach(externalData.keys) { key in
                Toggle(isOn: self.externalData.binding(for: key)) {
                    Text(key)
                }
            }
        }
    }
}
rraphael
  • 10,041
  • 2
  • 25
  • 33
  • Hmm, I'm getting an error at the `Text(key)` line in my view of `Type of Expression is ambiguious without more context`, does your code compile for you? – Phylliida Jul 12 '19 at 20:19
  • Figured it out, I need to do `ForEach(externalData.keys.identifiedby(\.self))` and then it works, thanks :) – Phylliida Jul 13 '19 at 01:20
  • 1
    This is a great solution! Thank you so much! – Stefan Vasiljevic Jan 05 '20 at 20:15
  • I am trying to use this for a DisclosureGroup but the group does not expand when clicked 'DisclosureGroup(isExpanded: self.categoriesShowing.binding(for: asset.category)) {...}'. However if the parent group is collapsed and expanded then the group does expand correctly - so I think SwiftUI is not picking up the model change resulting from clicking the expand button. Any suggestions? – Duncan Groenewald Jul 26 '20 at 13:12