Home > Net >  Why does calling a method via a pointer end up with a different result than calling via the method d
Why does calling a method via a pointer end up with a different result than calling via the method d

Time:08-11

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.

  • Related