David Cordero

Binding a Swift Dictionary to SwiftUI

Published on 17 Jun 2021

Creating a Form with a single Toggle in SwiftUI is a simple task. You can get a pretty nice result with just a few lines of code, as you can see here below:

struct ContentView: View {
    var body: some View {
        Form {
            Section {
                Toggle(isOn: $myToggleValue) {
                    Text("My Toggle")
                }
            }
        }
    }
    
    // MARK: - Private
    
    @State private var myToggleValue: Bool = false
}

But my use case was a little bit more complex. I did not have to display a single Toggle but a list of them, being the source of data a dynamic Dictionary that could contain a random number of elements.

My first attempt was trying to do it by extending the single Toggle example with a ForEach that iterates all the keys of my Dictionary:

struct ContentView: View {
    var body: some View {
        Form {
            Section {
                ForEach(allKeys, id: \.self) {
                    key in
                    Toggle(isOn: $myToggleValues[key]) {
                        Text("My Toggle")
                    }
                }
            }
        }
    }
    
    // MARK: - Private
    
    @State
    private var myToggleValues: [String: Bool] = [
        "One": false,
        "Two": true,
        "Three": true,
        "Caramba": false,
    ]
    
    private var allKeys: [String] {
        return myToggleValues.keys.sorted().map { String($0) }
    }
}

But sadly, it did not work. If you try to run this code, you will discover that it does not build. The compiler will show the following error in the line defining the Toggle:

Cannot convert value of type 'Binding<Bool?>' to expected argument type 'Binding<Bool>'

The error makes a lot of sense. The compiler is saying that $myToggleValues[key] is not guaranteed to have a Bool value. This is because the key could not be actually in the dictionary and Swift trying to access a nonexisting key in a dictionary will return nil.

Custom Binding

This is a situation where creating a custom Binding can be useful. Instead of using $ to get a standard Binding, I solved my problem by creating a custom Binding.

By defining a custom Binding you have the option to do whatever you want, for example to fallback to a false value in case the key is not found in the dictionary.

Here you can see the final result:

Here you have the code:

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        Form {
            Section {
                ForEach(allKeys, id: \.self) {
                    key in
                    Toggle(isOn: binding(for: key)) {
                        Text(key)
                    }
                }
            }
        }
    }
    
    // MARK: - Private
    
    @State
    private var myToggleValues: [String: Bool] = [
        "One": false,
        "Two": true,
        "Three": true,
        "Caramba": false,
    ]
    
    private var allKeys: [String] {
        return myToggleValues.keys.sorted().map { String($0) }
    }
    
    private func binding(for key: String) -> Binding<Bool> {
        return Binding(get: {
            return self.myToggleValues[key] ?? false
        }, set: {
            self.myToggleValues[key] = $0
        })
    }
}