I'm experiencing poor SwiftUI performance due to nested value types triggering redraws in the view hierarchy. Here's an example:
I have data model with nested parent/child/grandchild relationships:
struct Parent: Identifiable, Hashable {
let id: UUID
var children = [Child]()
}
struct Child: Identifiable, Hashable {
let id: UUID
var children = [Grandchild]()
}
struct Grandchild: Identifiable, Hashable {
let id: UUID
var name = ""
}
For each model I have a SettingsView
that accepts a binding to the model, and navigates to its own children if any:
struct ParentSettingsView: View {
@Binding var parent: Parent
var body: some View {
List(parent.children) { child in
NavigationLink(child.id.uuidString, value: child)
}
.navigationDestination(for: Child.self) { child in
let idx = parent.children.firstIndex(where: {$0.id == child.id})!
ChildSettingsView(child: $parent.children[idx])
}
}
}
// ChildSettingsView is basically identical
Finally at the bottom of the stack is GrandchildSettingsView
:
struct GrandchildSettingsView: View {
@Binding var grandchild: Grandchild
var body: some View {
TextField("Name", text: $grandchild.name)
}
}
Typing a name in GrandchildSettingsView
causes the entire view hierarchy to redraw with every keystroke, because a mutation to Grandchild
is also a mutation to Child
, and to Parent
, etc. This causes very bad performance, and other side effects.
Migrating the full data model to @Observable reference types technically works, but it leads to other issues when dealing with Sendable
, Codable
, etc. Using structs is far more lightweight for this application.
How can we improve SwiftUI performance with nested value types like these?
I'm experiencing poor SwiftUI performance due to nested value types triggering redraws in the view hierarchy. Here's an example:
I have data model with nested parent/child/grandchild relationships:
struct Parent: Identifiable, Hashable {
let id: UUID
var children = [Child]()
}
struct Child: Identifiable, Hashable {
let id: UUID
var children = [Grandchild]()
}
struct Grandchild: Identifiable, Hashable {
let id: UUID
var name = ""
}
For each model I have a SettingsView
that accepts a binding to the model, and navigates to its own children if any:
struct ParentSettingsView: View {
@Binding var parent: Parent
var body: some View {
List(parent.children) { child in
NavigationLink(child.id.uuidString, value: child)
}
.navigationDestination(for: Child.self) { child in
let idx = parent.children.firstIndex(where: {$0.id == child.id})!
ChildSettingsView(child: $parent.children[idx])
}
}
}
// ChildSettingsView is basically identical
Finally at the bottom of the stack is GrandchildSettingsView
:
struct GrandchildSettingsView: View {
@Binding var grandchild: Grandchild
var body: some View {
TextField("Name", text: $grandchild.name)
}
}
Typing a name in GrandchildSettingsView
causes the entire view hierarchy to redraw with every keystroke, because a mutation to Grandchild
is also a mutation to Child
, and to Parent
, etc. This causes very bad performance, and other side effects.
Migrating the full data model to @Observable reference types technically works, but it leads to other issues when dealing with Sendable
, Codable
, etc. Using structs is far more lightweight for this application.
How can we improve SwiftUI performance with nested value types like these?
Share Improve this question edited Mar 14 at 22:39 Hundley asked Mar 14 at 21:51 HundleyHundley 3,6075 gold badges29 silver badges48 bronze badges 11 | Show 6 more comments1 Answer
Reset to default 1Try this approach using a extra @State variable
as mentioned by @CouchDeveloper,
and a .onAppear
and .onDisappear
to update the binding grandchild.
The GrandchildSettingsView
will be refreshed as expected, but not the other Views.
Example code.
struct GrandchildSettingsView: View {
@Binding var grandchild: Grandchild
@State private var kid = Grandchild(id: UUID(), name: "")
// or @State private var kidName = "" ...
var body: some View {
let _ = print("-----> body GrandchildSettingsView") // <-- for testing
TextField("Name", text: $kid.name)
.onAppear {
kid = grandchild
}
.onDisappear{
grandchild = kid
}
}
}
.indicies
in yourList
– Paulw11 Commented Mar 14 at 22:34parent.children.indices
– timbre timbre Commented Mar 14 at 22:40.indices
was unnecessary here. Updated with cleaner code - but functionally identical for the problem. – Hundley Commented Mar 14 at 22:40