0

I have an app where two different structs depend on a separate piece of data, and when I change that data, both structs need to know about the change. I think I could solve the problem with re-factoring, or with @EnvironmentObject, but I want to understand why this code does't work. In SceneDelegate.swift in an iOS project, set the contentView with let contentView = StateTestTopView() and then create a file with this code:

import SwiftUI

struct Person { @Binding var name: String }
struct Character { @Binding var name: String }

struct StateTestTopView: View {
  @State private var name: String = "Bilbo"

  var body: some View {
    VStack {
      StateTestPickerView(name: $name)
      Spacer()
      Text("Global name selection is \(name)").font(.title)
    }
  }
}

struct StateTestPickerView: View {
  @Binding var name: String
  @State private var person: Person
  @State private var character: Character
  init(name: Binding<String>) {
    self._name = name
    self._person = State(initialValue: Person(name: name))
    self._character = State(initialValue: Character(name: name))
  }
  var body: some View {
    VStack{
      Picker(selection: $name,
             label: Text(verbatim: "Selected name: \(name)")) {
              ForEach(["Sam", "Gandalf", "Gollum"], id: \.self) { name in
                Text(name)
              }
      }
      Text("You picked \(name)")
      ShowPersonAndCharacter(
        person: self.$person, character: self.$character)
    }
  }
}

struct ShowPersonAndCharacter: View {
  @Binding var person: Person
  @Binding var character: Character

  var body: some View {
    Text("\(person.name) is great! \(character.name) is the same person!")
  }
}

The problem is the ShowPersonAndCharacter has @Bindings to the Person and Character structs, but the bound structs don't update when the underlying data changes. I suspect the problem is in the Picker view initializer:

  @Binding var name: String
  @State private var person: Person
  @State private var character: Character
  init(name: Binding<String>) {
    self._name = name
    self._person = State(initialValue: Person(name: name))
    self._character = State(initialValue: Character(name: name))
  }

Here I think the Person and Character structs get created with a snapshot of the bound String and don't actually get the binding. I'm not sure how to fix this, or even if my diagnosis is remotely correct.

Any ideas?

Thanks for your time!

2
  • 1
    This looks like misconception - Binding is intended to in view (not in model) and bound to some dynamic property. I'm not sure could it be possible to get operable what you try to do. Commented Apr 20, 2020 at 12:51
  • Thanks for your insights! Commented Apr 20, 2020 at 16:47

1 Answer 1

1

I agree with @Asperi, Binding are intended to be used in views and not models. In fact in this case, you don't need your model to have bindings at all. Since @Binding var name: String is a Binding in StateTestPickerView it will update the view by itself as soon as it's update. See the following modification of your code. This works perfectly fine for me:

struct Person { var name: String }
struct Character { var name: String }

struct StateTestTopView: View {
    @State private var name: String = "Bilbo"

    var body: some View {
        VStack {
            StateTestPickerView(name: $name)
            Spacer()
            Text("Global name selection is \(name)").font(.title)
        }
    }
}

struct StateTestPickerView: View {
    @Binding var name: String

    var body: some View {
        VStack{
            Picker(selection: $name,
                   label: Text(verbatim: "Selected name: \(name)")) {
                    ForEach(["Sam", "Gandalf", "Gollum"], id: \.self) { name in
                        Text(name)
                    }
            }
            Text("You picked \(name)")
            ShowPersonAndCharacter(person: Person(name: self.name),
                                   character: Character(name: self.name))
        }
    }
}

struct ShowPersonAndCharacter: View {
    var person: Person
    var character: Character

    var body: some View {
        Text("\(person.name) is great! \(character.name) is the same person!")
    }
}

Note that Binding property wrapper has been removed from the models. Also, StateTestPickerView directly calls the view using the name binding in following code: ShowPersonAndCharacter(person: Person(name: self.name), character: Character(name: self.name))

Sign up to request clarification or add additional context in comments.

1 Comment

Okay, thanks for your ideas. I agree this works here because the Person and Character structs are so simple. I can't adopt this approach for my real problem, but your answer coupled with @Aperi's suggests I need to re-factor and use a different approach.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.