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)
    }
}

tdl
  • 297
  • 3
  • 11

1 Answers1

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.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • 1
    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. – Sergio Bost Mar 09 '21 at 21:06
  • @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. – tdl Mar 09 '21 at 21:07
  • 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. – jnpdx Mar 09 '21 at 21:14
  • Came across this and was reminded of your issue: https://www.donnywals.com/observing-changes-to-managed-objects-across-contexts-with-combine/ – jnpdx Mar 11 '21 at 23:31
  • @jnpdx, thanks for the pointer. I'll check it out. – tdl Mar 13 '21 at 16:53