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:
- Use only one view model that integrates all functionalities you need.
- Create an
@ObservedObject
in your second view. - 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.