Here is a simple example of a more complex problem I'm having.
In this case, I have an array of Objects, and a struct that holds the state for them.
In the ContentView, I'm displaying a custom view, MyToggle, for each object, and passing the state via a Binding
struct Object: Identifiable {
let id = UUID()
let name: String
}
struct ObjectStates {
var states: [Object.ID: Bool] = [:]
subscript(objectId: Object.ID) -> Bool {
get { states[objectId, default: false] }
set { states[objectId] = newValue }
}
}
struct ContentView: View {
let objects = [Object(name: "Toggle 1"), Object(name: "Toggle 2"), Object(name: "Toggle 3"), Object(name: "Toggle 4"), Object(name: "Toggle 5")]
@State var objectStates = ObjectStates()
var body: some View {
List(objects) { object in
MyToggle(name: object.name, isOn: $objectStates[object.id])
}
}
}
struct MyToggle: View {
let name: String
@Binding var isOn: Bool
var body: some View {
let _ = print(name)
let _ = Self._printChanges()
Toggle(name, isOn: $isOn)
}
}
Each time a Toggle is changed, ObjectStates is updated, and all the subviews are redrawn. The Self._printChanges() is used to demonstrate this.
Is there a way to prevent all the subviews being redrawn when the state in the superview changes?
Interestingly, if I add another @State on the superview…
struct ContentView: View {
let objects = [Object(name: "Toggle 1"), Object(name: "Toggle 2"), Object(name: "Toggle 3"), Object(name: "Toggle 4"), Object(name: "Toggle 5")]
@State var objectStates = ObjectStates()
@State var isOn = false. // added this State
var body: some View {
List {
Toggle("Main", isOn: $isOn) // change it here
ForEach(objects) { object in
MyToggle(name: object.name, isOn: $objectStates[object.id])
}
}
}
}
The subviews are not redrawn when this state is updated.
First to the reason why this happens.
Both structs
ObjectStatesandObjectare value types the same as thestatesdictionary. If you change a value type it gets destroyed and a new copy with the changed values is created. This new entity has a new id. The@Stateproperty wrapper detects changes by changes of the id of its wrapped value. That´s the reason we are using structs in SwiftUI. If you would use classes changes won´t reflect into the UI.When an
@Stateproperty is changed it sends itsobjectWillChangepublisher. The SwiftUI View now tries to evaluate if it needs to change the presented view. (Keep in mind the Views we are writing in SwiftUI are just a description of the view not the view itself.) To evaluate changes it calls the body var. Now when SubViews depend on any changed var it needs to evaluate these too. It can´t just guess if something changed or not. It has to run thebodyand compare it to the previous one.That´s what you are seeing here. The subviews depend on
objectStatesso it has to call allMyTogglebody vars to determine if they changed or not. It won´t call them in your second example because they do not depend on the changed@State var isOn.Conclusion:
I don´t think there is something wrong with your approach. Calling the body var multiple times shouldn´t be of any concern. They should be lightweighted and easy to destroy / create anyway. List should only call the body var of the visible elements, so there should be no impact on performance if you have larger collections.
There is a work around for this. But it will have drawbacks as it uses a class to avoid the reevaluation of the subviews.
Change the struct to a class and implement ObservableObject
Declare it
@StateObjectinside your View. This is needed to bind the lifecycle of the class to the view.Now you can use the
Toggles without recreating theMyTogglestruct.And of course the mandatory link to the video that´s more or less a must watch if working with SwiftUI -> Demystify SwiftUI