Home > Mobile >  SwiftUI issue with changing @Binding object in the second view
SwiftUI issue with changing @Binding object in the second view

Time:02-28

I want to change the object in the binding from the second screen but it's not working as i thought. I can change properties of the object but not the actual reference. If someone can explain why is that and what is the best way to achieve this ? From what i read @Binding doesn't work very well outside View. Also i included a workaround where i use second published property to update the ui but it's not a good solution in my opinion.. Am i doing something wrong or it's just how @Binding works ? (You can just copy and paste the code to see what i mean)

class TestModel {
   var text:String?
   
   init(text: String?) {
       self.text = text
   }
}

class FirstViewVM: ObservableObject {
   @Published var model:TestModel?
   @Published var showSecondView = false
}

struct FirstView: View {
   @StateObject var viewModel = FirstViewVM()
   
   var body: some View {
       VStack {
           Text(viewModel.model?.text ?? "n/a")
               .padding()
           Button("New model") {
               viewModel.model = TestModel(text: "New model from screen 1")
           }
           .padding()
           Button("Show screen 2") {
               viewModel.showSecondView = true
           }
           .padding()
       }
       .sheet(isPresented: $viewModel.showSecondView) {
           NavigationView {
               //First check the problem.. then use the workaround view to fix it....
               SecondView(viewModel: SecondViewVM(model: $viewModel.model))
//                SecondViewWorkaround(viewModel: SecondViewVMWorkaround(model: $viewModel.model))
           }
       }
   }
}


class SecondViewVM: ObservableObject {
   @Binding var model:TestModel?
   
   init(model: Binding<TestModel?>) {
       _model = model
   }
   
}

struct SecondView: View {
   @StateObject var viewModel:SecondViewVM
   
   var body: some View {
       VStack {
           Text(viewModel.model?.text ?? "n/a")
               .padding()
           Button("Edit model") {
               viewModel.objectWillChange.send()
               viewModel.model?.text = "Edited from screen 2"
           }
           .padding()
           //This will not update the UI
           Button("New model") {
               viewModel.objectWillChange.send()
               viewModel.model = TestModel(text: "New model from screen 2")
           }
           .padding()
       }
   }
}


//Workaround....First check the views above..
class SecondViewVMWorkaround: ObservableObject {
   @Binding var model:TestModel? {
       didSet{
           modelUsedForUI = model //Passing the changes to the other model that is used for ui
       }
   }
   @Published var modelUsedForUI:TestModel? //Added another published var that will update the ui
   
   init(model: Binding<TestModel?>) {
       _model = model
       modelUsedForUI = model.wrappedValue
   }
   
   
}

struct SecondViewWorkaround: View {
   @StateObject var viewModel:SecondViewVMWorkaround
   
   var body: some View {
       VStack {
           Text(viewModel.modelUsedForUI?.text ?? "n/a")
               .padding()
           Button("Edit model") {
               viewModel.objectWillChange.send()
               viewModel.model?.text = "Edited from screen 2"
           }
           .padding()
           Button("New model") {
               viewModel.objectWillChange.send()
               viewModel.model = TestModel(text: "New model from screen 2")
           }
           .padding()
       }
   }
}

CodePudding user response:

I think there is some over-complication in your code when you created a second view model to pass to the second view, and tried to link two view models instead of linking the views. @Binding is a wrapper to use in a View when passing @State variables. You are instead using it between two view models, I don't think it can work as you intend.

I suggest a different approach:

  1. Use only one view model that integrates all functionalities you need.
  2. Create an @ObservedObject in your second view.
  3. Pass the view model from your first to your second view.

The example below works, you want to try it:

1. Only one view model


class TestModel {
   var text:String?
   
   init(text: String?) {
       self.text = text
   }
}

class FirstViewVM: ObservableObject {
   @Published var model:TestModel?
   @Published var showSecondView = false
    
    // Merge here the functionalities of your second view model, or
    // just create an instance, like:
    // @Published var secondVM = SecondViewModel()
}

2. and 3. Pass the same view model from the first to the second view

struct FirstView: View {
    @StateObject var viewModel = FirstViewVM()
    
    var body: some View {
        VStack {
            Text(viewModel.model?.text ?? "n/a")
                .padding()
            Button("New model") {
                viewModel.model = TestModel(text: "New model from screen 1")
            }
            .padding()
            Button("Show screen 2") {
                viewModel.showSecondView = true
            }
            .padding()
        }
        .sheet(isPresented: $viewModel.showSecondView) {
            NavigationView {
                
                // Just pass the same model from one view to another
                SecondView(viewModel: viewModel)
            }
        }
    }
}

struct SecondView: View {
    @ObservedObject var viewModel: FirstViewVM      // Read your model here
   
   var body: some View {
       VStack {
           Text(viewModel.model?.text ?? "n/a")
               .padding()
           Button("Edit model") {
               viewModel.objectWillChange.send()
               viewModel.model?.text = "Edited from screen 2"
           }
           .padding()
           
           // Now it updates
           Button("New model") {
               viewModel.objectWillChange.send()
               viewModel.model = TestModel(text: "New model from screen 2")
           }
           .padding()
       }
   }
}

CodePudding user response:

Yes you're doing something wrong. With Swift and SwiftUI we use the advantages of value types that fix the consistency problems you are facing. So your model should be a struct, e.g.

struct TestModel {
   var text: String
   
   init(text: String) {
       self.text = text
   }
}

Then an object is used manage the life-cycle and side-effect of the model structs, e.g.

class Model: ObservableObject {
   @Published var testModel: TestModel?

   // funcs to load and save etc.
    func newTestModel(text: String) {
         testModel = TestModel(text: text)
    }
}

Also in SwiftUI we don't design our Views based on screens, we break them up depending on the data they need. So they should be something like this:

struct ContentView: View {
    @StateObject var model = Model()
    @State var showSecondView = false

    var body: some View {
        VStack {
            if let testModel = model.testModel {
                ContentView2(testModel: testModel)
            }
            Button("New model") {
                model.newTestModel(text: "from screen 1")
            }
            .padding()
            Button("Show screen 2") {
                showSecondView = true
            }
            .padding()
        }
        .sheet(isPresented: $showSecondView) {
            NavigationView {
                
                // Just pass the same model from one view to another
                SecondView(model: model)
            }
        }
    }
}

struct ContentView2: View {
    let testModel: TextModel

    var body: some View {
        Text(testModel.text ?? "n/a")
        .padding()
    }
}

struct SecondView: View {
    @ObservableObject var model: Model
   
   var body: some View {
       VStack {
           if let testModel = model.testModel {
               Button("Edit model") {
                   testModel.text = "Edited from screen 2"
               }
               .padding()
           }
               // Now it updates
           Button("New model") {
               model.newTestModel(text: "New model from screen 2")
           }
           .padding()
       }
   }
}

Note: when we want to present data on a sheet we use sheet(item:) rather than the boolean version.

  • Related