3

Simple question to ask but I probably forgot something in the code.
Let's explain better using an image:

enter image description here

I need basically to update "COUNT" and "PRICE" while selecting/deselecting items.


I have a code structure like this:

Model:

class ServiceSelectorModel: Identifiable, ObservableObject {
    var id = UUID()

    var serviceName: String
    var price: Double
    
    init(serviceName: String, price: Double) {
        self.serviceName = serviceName
        self.price = price
    }

    @Published var selected: Bool = false
}

ViewModel:

class ServiceSelectorViewModel: ObservableObject {
    @Published var services = [ServiceSelectorModel]()

    init() {
        self.services = [
            ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
            ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
            ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
        ]
    }
}

ToggleView

struct ServiceToggleView: View {
    @ObservedObject var model: ServiceSelectorModel
    
    var body: some View {
        VStack(alignment: .center) {
            HStack {
                Text(model.serviceName)
                
                Toggle(isOn: $model.selected) {
                    Text(String(format: "€ +%.2f", model.price))
                        .frame(maxWidth: .infinity, alignment: .trailing)
                }
            }
            .background(model.selected ? Color.yellow : Color.clear)
        }
    }
}

ServiceSelectorView

struct ServiceSelectorView: View {

    @ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()

    var body: some View {
        VStack {
            VStack(alignment: .leading) {
                ForEach (serviceSelectorVM.services, id: \.id) { service in
                    ServiceToggleView(model: service)
                }
            }

            let price = serviceSelectorVM.services.filter{ $0.selected }.map{ $0.price }.reduce(0, +)

            Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.selected }.count)")
            Text(String(format: "TOTAL PRICE: €%.2f", price))    
        }
    }
}

In this code I'm able to update the selected status of the model but the view model that contains all the models and should refresh the PRICE not updates.
Seems that the model in the array doesn't change.

what have i forgotten?

2 Answers 2

2

Probably most simple is to make your model as value type, then changing its properties the view model, holding it, will be updated. And to update view to use binding to those values

struct ServiceSelectorModel: Identifiable {
    var id = UUID()

    var serviceName: String
    var price: Double
    
    init(serviceName: String, price: Double) {
        self.serviceName = serviceName
        self.price = price
    }

    var selected: Bool = false
}

struct ServiceToggleView: View {
    @Binding var model: ServiceSelectorModel
...
}

...

ForEach (serviceSelectorVM.services.indices, id: \.self) { i in
    ServiceToggleView(model: $serviceSelectorVM.services[i])
}

Note: written inline, not tested, some typo might needed to be fixed

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

1 Comment

don't worry about the typo, I need only a suggestion on how to fix, not to copy/paste :)
1

You have created two different Single source of truth if you notice carefully. That’s the reason parent is not updating, as it’s not linked to child.

One way is to create ServiceSelectorModel as a struct, and pass services array as @Binding in child view. Below is the working example.

ViewModel-:

struct ServiceSelectorModel {
    
    var id = UUID().uuidString
    var serviceName: String
    var price: Double
    var isSelected:Bool = false
    
    init(serviceName: String, price: Double) {
        self.serviceName = serviceName
        self.price = price
    }
    
}

class ServiceSelectorViewModel: ObservableObject,Identifiable {
    
    @Published var services = [ServiceSelectorModel]()
    
    var id = UUID().uuidString
    init() {
        self.services = [
            ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
            ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
            ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
        ]
    }
}

Views-:

struct ServiceToggleView: View {
    @Binding var model: [ServiceSelectorModel]
    
    var body: some View {
        VStack(alignment: .center) {
            ForEach(0..<model.count) { index in
                HStack{
                    Text(model[index].serviceName)
                    Toggle(isOn: $model[index].isSelected) {
                        Text(String(format: "€ +%.2f", model[index].price))
                            .frame(maxWidth: .infinity, alignment: .trailing)
                    }
                }.background(model[index].isSelected ? Color.yellow : Color.clear)
            }
            
        }
    }
}

struct ServiceSelectorView: View {
    
    @ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
    
    var body: some View {
        VStack {
            VStack(alignment: .leading) {
                ServiceToggleView(model: $serviceSelectorVM.services)
            }
            
            let price = serviceSelectorVM.services.filter{ $0.isSelected }.map{ $0.price }.reduce(0, +)
            
            Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.isSelected }.count)")
            Text(String(format: "TOTAL PRICE: €%.2f", price))
        }
    }
}

Comments

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.