Editable Bindings / Mutating a List via ForEach in SwiftUI

by @ralfebert · updated July 13, 2021
SwiftUI, Xcode 13 & iOS 15
Advanced iOS Developers

Let's say you're using SwiftUI's ForEach and want to add a View like a Toggle that changes the underlying data:

struct Light: Identifiable {
    var id: Int
    var active: Bool = false
}

class HomeModel: ObservableObject {
    @Published var lights = (1 ... 5).map { Light(id: $0) }
}

struct EnumeratedListView: View {

    @StateObject var homeModel = HomeModel()

    var body: some View {
        Form {
            ForEach(self.homeModel.lights) { light in
                Text("Light \(light.id)")
            }
        }
    }

}

How can a Toggle be added and mutate the value in the HomeModel?

Solution for Xcode 13

In Xcode 13 this is straightforward using the new binding syntax for list elements:

import Foundation
import SwiftUI

struct EnumeratedListView: View {

    @StateObject var homeModel = HomeModel()

    var body: some View {
        Form {
            ForEach( $homeModel.lights) { $light in
                Toggle("Light \(light.id)", isOn: $light.active)
            }
        }
    }

}

This will also work on older iOS versions, you only need to build with Xcode 13 / Swift 5.5.
There is a deep dive into how this works behind the scenes here:
SwiftUI List Bindings - Behind the Scenes

Solution for Xcode 12

You might be tempted to use ForEach(Array(list.enumerated())). Unfortunately, this is incorrect and might cause the wrong data to be edited when the array changes.

A solution that improves this comes from Apple release notes. You need to add the IndexedCollection helper to your project and can then use .indexed() on the list:

Form {
    ForEach(self.homeModel.lights.indexed(), id: \.element.id) { idx, light in
        Toggle("Light \(light.id)", isOn: $homeModel.lights[idx].active)
    }
}

See also: