Closures in SwiftUI
Main pages:
In general, closures cannot be compared for equality. This can cause unnecessary view invalidations. For example, consider the view below. Instead of passing value
as an Int
we pass a closure that computes the Int
. Every time the unrelated
state property changes, the Nested
body is recomputed (we can see this because of the color change). If we pass the value
as an Int
instead, the recompution doesn’t happen.
struct Nested: View {
var value: () -> Int
var body: some View {
Text("Value: \(value())")
.background(Color(hue: .random(in: 0...1), saturation: 0.8, brightness: 0.8))
}
}
struct ContentView: View {
@State var value = 0
@State var unrelated = 0
var body: some View {
VStack {
Nested(value: { value })
Button("Unrelated: \(unrelated)") {
unrelated += 1
}
}
}
}
In practice, this might not be a big deal for most views, but it could be something to keep in mind if you’re seeing more invalidations than expected.
SwiftUI itself doesn’t typically pass functions through the environment. Instead, it uses defunctionalization to pass a value that implements callAsFunction
. This is done by e.g. DismissAction
, RefreshAction
and OpenWindowAction
.
Another option if you need to pass closures down the environment is to use a (stable) object. The identity of the object doesn’t change and SwiftUI won’t need to rerender views because of this.
SwiftUI itself could have been modeled as view functions rather than View
structs. Instead, views are defunctionalized to be able to compare for equality.