Editable Bindings / Mutating a List via ForEach in SwiftUI
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:
- Full example: SwiftUIPlayground/EnumeratedListView.swift
- Modeling app state using Store objects in SwiftUI for a more elaborated example of using IndexedCollection.
- How does the Apple-suggested .indexed() property work in a ForEach?