In the example following, I'm calling an instance method of a View using the method directly (saveTitle), and via a pointer to that method (saveAction).
When calling the method, I am passing in the current value of the title variable. When I call it directly, the current value matches the value inside the method. When I call it via a pointer to the method, the value inside is the value it was when the struct was first instantiated.
It is almost like there is a new instance of the Struct being created, but without the init method being called a second time.
I suspect this has something to do with how SwiftUI handles @State modifiers, but I'm hoping someone out there will have a better understanding, and be able to enlighten me a bit.
Thanks much for your consideration :)
import SwiftUI
struct EditContentView: View {
@Binding var isPresented: Bool
@State var title: String
var saveAction: ((String) -> Void)?
var body: some View {
TextField("new title", text: $title)
Button("save") {
print("calling saveText")
// this call works as expected, the title inside the saveTitle method is the same as here
saveTitle(title)
print("now calling saveAction, a pointer to saveTitle")
// with this call the title inside the method is different than the passed in title here
// even though they are theoretically the same member of the same instance
saveAction!(title)
isPresented = false
}
}
init(isPresented: Binding<Bool>, title: String) {
print("this method only gets called once")
self._isPresented = isPresented
self._title = State(initialValue: title)
saveAction = saveTitle
}
func saveTitle(_ expected: String) {
if (expected == title) {
print("the title inside this method is the same as before the call")
}
else {
print("expected: \(expected), but got: \(title)")
}
}
}
struct ContentView: View {
@State var title = "Change me"
@State var showingEdit = false
var body: some View {
Text(title)
.onTapGesture { showingEdit.toggle() }
.sheet(isPresented: $showingEdit) {
EditContentView(isPresented: $showingEdit, title: title)
}
}
}
CodePudding user response:
I don't think this is related to @State
. It is just a natural consequence of structs having value semantics, i.e.
struct Foo {
init() {
print("This is run only once!")
}
var foo = 1
}
var x = Foo() // Prints: This is run only once!
let y = x // x and y are now two copies of the same *value*
x.foo = 2 // changing one of the copies doesn't affect the other
print(y.foo) // Prints: 1
Your example is essentially just a little more complicated version of the above. If you understand the above, then you can easily understand your SwiftUI case, We can actually simplify your example to one without all the SwiftUI distractions:
struct Foo {
var foo = 1
var checkFooAction: ((Int) -> Void)?
func run() {
checkFoo(expectedFoo: foo)
checkFooAction!(foo)
}
init() {
print("This is run only once!")
checkFooAction = checkFoo
}
func checkFoo(expectedFoo: Int) {
if expectedFoo == foo {
print("foo is expected")
} else {
print("expected: \(expectedFoo), actual: \(foo)")
}
}
}
var x = Foo()
x.foo = 2 // simulate changing the text in the text field
x.run()
/*
Output:
This is run only once!
foo is expected
expected: 2, actual: 1
*/
What happens is that when you do checkFooAction = checkFoo
(or in your case, saveAction = saveTitle
), the closure captures self
. This is like the line let y = x
in the first simple example.
Since this is value semantics, it captures a copy. Then the line x.foo = 2
(or in your case, the SwiftUI framework) changes the other copy that the closure didn't capture.
And finally, when you inspect what foo
(or title
) by calling the closure, you see the unchanged copy, analogous to inspecting y.foo
in the first simple example.
If you change Foo
to a class, which has reference semantics, you can see the behaviour change. Because this time, the reference to self
is captured.
See also: Value and Reference Types
Now you might be wondering, why does saveAction = saveTitle
capture self
? Well, notice that saveTitle
is an instance method, so it requires an instance of EditContentView
to call, but in your function type, (String) -> Void
, there is no EditContentView
at all! This is why it "captures" (a copy of) the current value of self
, and says "I'll just always use that".
You can make it not capture self
by including EditContentView
as one of the parameters:
// this doesn't actually need to be optional
var saveAction: (EditContentView, String) -> Void
assign it like this:
saveAction = { this, title in this.saveTitle(title) }
then provide self
when calling it:
saveAction(self, title)
Now you won't get different copies of self
flying around.