1

Binding is not acting like expected and I'd appreciate insights into what's going on.

Here's code that illustrates the issue. A class Model object called "Project" contains an array of Strings called "name". The code passes a Binding for name to a ViewModel of type ProjectVM for use in View. In the View's List I can delete a row, corresponding to deleting one of the elements of the String array, but then it comes right back.

This code should be operating on the original array since it's using a Binding, but apparently that's not what's happening. Any ideas?

It works as expected if the root object is an @State var of names (see commented-out code) instead of being a property of Project.

Using Xcode 12.4 with Swift 5

@main
struct Try_ArrayBindingApp: App {
    @State var project = Project()
    //@State var names = [ "a", "b", "c" ]
    var body: some Scene {
        WindowGroup {
            ProjectV(pVM: ProjectVM(names: $project.names))
            //ProjectV(pVM: ProjectVM(names: $names))
        }
    }
}

class Project { var names = [ "one", "two", "three"] }

class ProjectVM: ObservableObject {
    @Binding var names: [String]
    
    init(names: Binding<[String]> ) { self._names = names }
    
    func delete(at offsets: IndexSet) {
        names.remove(atOffsets: offsets)
    }
}

struct ProjectV: View {
    @ObservedObject var pVM: ProjectVM
    
    var body: some View {
        VStack {
            List {
                ForEach(pVM.names, id: \.self) { n in
                    Text(n)
                }
                .onDelete(perform: delete)
            }
        }
    }
    
    private func delete(at offsets: IndexSet) {
        pVM.delete(at: offsets)
    }
}

3
  • first thing don’t use state wrappers with classes.Another thing @bindings are for views, mark your [String] published. In main change from state to StateObject Commented Mar 9, 2021 at 20:49
  • @TusharSharma, how can I use $ to pass in a binding if I don't use state? Commented Mar 9, 2021 at 20:53
  • Check the answer by @jnpdx Commented Mar 9, 2021 at 20:54

1 Answer 1

2

By holding the initial @State in the parent view and then @Binding that to the observable object, which then gets sent to the child view, at the least, flow of data definitely gets confusing. I'm actually not convinced that it shouldn't behave like you think, but it's a confusing mental model to think about and not something you see real commonly in SwiftUI.

A more common model would be to hold the state in an ObservableObject, which is owned by the parent view:

@main
struct Try_ArrayBindingApp: App {
    @StateObject var project = ProjectVM(names: [ "one", "two", "three"])
    var body: some Scene {
        WindowGroup {
          ProjectV(pVM: project)
        }
    }
}

class ProjectVM: ObservableObject {
    @Published var names: [String]
    
    init(names: [String]) {
        self.names = names
    }
    
    func delete(at offsets: IndexSet) {
        names.remove(atOffsets: offsets)
    }
}

struct ProjectV: View {
    @ObservedObject var pVM: ProjectVM
    
    var body: some View {
        VStack {
            List {
                ForEach(pVM.names, id: \.self) { n in
                    Text(n)
                }
                .onDelete(perform: delete)
            }
        }
    }
    
    private func delete(at offsets: IndexSet) {
        pVM.delete(at: offsets)
    }
}

Note that the names is now a @Published property.

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

5 Comments

I agree with all of that, except I would make the ObservedObject an StateObject in the parent view. StateObject is the equivalent of State for classes and its especially for views that own other views that wish to use that data.
@jnpdx, thanks for the suggestion. It works but what I'm trying to do is have a Model object that's a class (for several reasons including it's easier to use it with Core Data) - that's why I had a "Project" class. The ViewModel object ProjectVM is used to access just the "names" instance var using a binding so that its corresponding View can just operate on that array. I can't seem to get that to work.
I see. CoreData has a lot of convenience methods and property wrappers built in to help you use it with SwiftUI. Trying to wrap CoreData inside other ObservableObjects and use them with Binding/Published (which really work better with structs) is probably an uphill battle.
Came across this and was reminded of your issue: donnywals.com/…
@jnpdx, thanks for the pointer. I'll check it out.

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.