Why is parameter for onChange(of:perform:) captured?

Viewed 1327

I was using the onChange(of:perform:) SwiftUI modifier. I then wanted to get the old value, to compare it with the new value. I read the documentation, which says:

The previous value may be captured by the closure to compare it to the new value.

With an example:

.onChange(of: playState) { [playState] newState in
    model.playStateDidChange(from: playState, to: newState)
}

My question is why in the example is playState captured in the [ ]? The playState value could easily be accessed without passing it in. In addition, this isn't part of a class, so I assume there is no way to create a strong reference by capturing self of some sort.

Why is the example written like this?

1 Answers

Under "normal" circumstances, a closure defined inside an immutable value-type (e.g. a struct) captures its value, which doesn't change, so all is good.

struct Foo {
  var a = "original"
  
  func makeFn() -> () -> Void {
     return { print(a) }
  }
}

var foo = Foo()
let fn = foo.makeFn()
foo.a = "changed"

fn() // "original"

But with @State, the actual value is stored in some global storage that SwiftUI maintains, so it basically behaves as if it's having reference semantics.

When the closure is called, the state value has already changed, so doing print(a) as above accesses a value through the @State property wrapper, which retrieves the then-updated value.

To counter that, you can capture the property into the closure's local variable with a capture list:

return { [a] in print(a) }

This is of course a simplified example, and SwiftUI might be doing other things behind the scenes, but I think it conveys the point.


To see the difference in SwiftUI, try the following:

.onChange(of: playState) { [playState] newState in
    print(playState, self.playState, newState)
}

The output would be something like this:

original new new

playState is the captured local variable at the time the closure was defined (i.e. when body was computed), self.playState accesses the value via @State, which has changed, and newState is obviously the passed-in parameter with the new value.

Related