Search code examples
iosloopsswiftuiobservableobjectswiftui-foreach

ObservableObject not updating view in nested loop SWIFTUI


Regarding the following project :

You have an amountSum of 100 When you click on one user "plus" button, this specific user have to pay this amount but if you click on multiple user "plus" button, the amount to pay is divided between them equally.

Any idea how I can update the entire Model2.MustPayM2 prop when I click on the "plus" button please ?

import SwiftUI

struct Model1: Identifiable, Codable {
    var id: String = UUID().uuidString
    var nameM1: String
    var amountM1: Double
    var amountSumM1: Double = 100
    var arrayM2: [Model2]
    var isVisible: Bool = false
}


struct Model2: Identifiable, Codable {
    var id: String = UUID().uuidString
    var nameM2: String
    var amountM2: Double = 0
    var mustPayM2: Bool = false
}


class ViewModel1: ObservableObject {
    
    @Published var Publi1: Model1
    @Published var Publi1s: [Model1] = []
    @Published var Publi2: Model2
    @Published var Publi2s: [Model2] = []

    init() {
        let pub2 = Model2(nameM2: "init")
        let pub1 = Model1(nameM1: "init", amountM1: 0, arrayM2: [pub2])
        
        self.Publi2 = pub2
        self.Publi1 = pub1
        
        var newPub1s: [Model1] = []
        for i in (0..<5) {
            let newNameM1 = "name\(i+1)"
            let newAmountM1 = Double(i+1)
            var newModel1 = Model1(nameM1: newNameM1, amountM1: newAmountM1, arrayM2: [pub2])
            var newPub2s: [Model2] = []
            for i in (0..<5) {
                let newNameM2 = "\(newNameM1)-user\(i+1)"
                let newModel2 = Model2(nameM2: newNameM2)
                newPub2s.append(newModel2)
            }
            newModel1.arrayM2 = newPub2s
            newPub1s.append(newModel1)
        }
        
        Publi1s = newPub1s
        Publi1 = newPub1s[0]
        Publi2s = newPub1s[0].arrayM2
        Publi2 = newPub1s[0].arrayM2[0]
    }

}


struct View1: View {
    
    @EnvironmentObject var VM1: ViewModel1
    
    @State private var tt: String = ""
    
    private let screenHeight = UIScreen.main.bounds.height
    
    var body: some View {
        ZStack {
            VStack {
                ForEach(0..<VM1.Publi2s.count, id: \.self) { i in
                    
                    Text("\(VM1.Publi2s[i].nameM2)")
                    
                    Text(tt)
                    
                    Button {
                        VM1.Publi2s[i].mustPayM2.toggle()
                        var a = VM1.Publi2s.filter { $0.mustPayM2 == true }
                        let b = VM1.Publi1.amountM1 / Double(a.count)
                        
                        // How can I update the new props between all users ??
                        // for j in 0..<a.count {
                        //     a[j].amountM2 = b
                        // }
                        
                    } label: {
                        Image(systemName: "plus")
                        
                    }
                }
                
                Spacer()
                
                Button {
                    VM1.Publi1.isVisible.toggle()
                } label: {
                    Text("SHOW ME")
                    
                }
                
                Spacer()
                
            }
            
            View2()
                .offset(y: VM1.Publi1.isVisible ? 0 : screenHeight)
        }
    }
}


struct View2: View {
    
    @EnvironmentObject var VM1: ViewModel1
    
    var body: some View {
        VStack {
            Spacer()
            ForEach(0..<VM1.Publi2s.count, id: \.self) { i in
                
                Text("\(VM1.Publi2s[i].amountM2)")
                
            }
        }
    }
}


struct View2_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            View1()
        }
        .environmentObject(ViewModel1())
    }
}

Solution

  • You implementation seems overly complicated and error prone. I´ve practically rewritten the code for this. I´ve added comments to make it clear what and why I have done certain things. If you don´t understand why, don´t hesitate to ask a question. But please read and try to understand the code first.

    //Create one Model containing the individuals
    struct Person: Identifiable, Codable{
        var id = UUID()
        var name: String
        var amountToPay: Double = 0.0
        var shouldPay: Bool = false
    }
    
    //Create one Viewmodel
    class Viewmodel:ObservableObject{
        //Entities being observed by the View
        @Published var persons: [Person] = []
        
        init(){
            //Create data
            persons = (0...4).map { index in
                Person(name: "name \(index)")
            }
        }
        
        //Function that can be called by the View to toggle the state
        func togglePersonPay(with id: UUID){
            
            let index = persons.firstIndex { $0.id == id}
            
            guard let index = index else {
                return
            }
            //Assign new value. This will trigger the UI to update
            persons[index].shouldPay.toggle()
        }
        
        //Function to calculate the individual amount that should be paid and assign it
        func calculatePayment(for amount: Double){
            //Get all persons wich should pay
            let personsToPay = persons.filter { $0.shouldPay }
            //Calcualte the individual amount
            let individualAmount = amount / Double(personsToPay.count)
            //and assign it. This implementation will trigger the UI only once to update
            persons = persons.map { person in
                var person = person
                person.amountToPay = person.shouldPay ? individualAmount : 0
                return person
            }
            
        }
    }
    
    struct PersonView: View{
        //pull the viewmodel from the environment
        @EnvironmentObject private var viewmodel: Viewmodel
        //The Entity that holds the individual data
        var person: Person
        var body: some View{
            VStack{
                HStack{
                    Text(person.name)
                    Text("\(person.amountToPay, specifier: "%.2f")$")
                }
                Button{
                    //toggle the state
                    viewmodel.togglePersonPay(with: person.id)
                } label: {
                    //Assign label depending on person state
                    Image(systemName: "\(person.shouldPay ? "minus" : "plus")")
                }
            }
        }
    }
    
    struct ContentView: View{
        //Create and observe the viewmodel
        @StateObject private var viewmodel = Viewmodel()
        var body: some View {
            VStack {
                // Create loop to display person.
                // Don't iterate over the indices this is bad practice
                // iterate over the items themselves
                ForEach(viewmodel.persons){ person in
                    PersonView(person: person )
                        .environmentObject(viewmodel)
                        .padding(10)
                }
                
                Button{
                    //call the func to calculate the result
                    viewmodel.calculatePayment(for: 100)
                }label: {
                    Text("SHOW ME")
                }
            }
        }
    }