0
votes

I am trying to use an @ObservableObject viewModel which is declared as @ObservedObject inside my view struct. The problem is that when the viewModel changes it's "domains" property @Published var, the UI is not updating. Also, I change domains inside getDomains() function which is called inside init() {}. Looks like it is called twice, why is that happening? Here's my code for viewModel:

import Combine

class DomainsViewModel: ObservableObject {

    @Published var domains: [InterestDomain] = [InterestDomain]()
    
    init() {
        self.getDomains { (response) in
            print(response)
        }
    }
    
    func getDomains(completion: @escaping (Bool) -> Void) {
        NetworkEngine.shared.appNetwork.getInterestDomains { result in
            self.domains.removeAll()
            switch result {
            case .success(let domainsOfInterest):
                if let domainsList = domainsOfInterest {
                    self.domains = domainsList
                }
                completion(true)
            case .failure(_):
                completion(false)
            }
        }
    }

}

Code for the view: import Foundation import SwiftUI import Combine

struct DomainsOfInterestView: View {

    @ObservedObject var viewModel: DomainsViewModel = DomainsViewModel()
    
    @State var isActive = true
    
    var body: some View {
            VStack(alignment: .center) {
                HStack {
                    Text("Choose domains of interest for your profile")
                        .font(.headline)
                    Spacer()
                }.padding(.bottom, 16)
                
                ForEach(viewModel.domains.indices) { index in
                    DomainOfInterestElement(isActive: self.$isActive, domain: self.viewModel.domains[index].name)
                }

                OrangeButton(action: {
                }) {
                    Text("Save")
                }.padding(.top, 30)
                    .padding([.leading, .trailing], 30)
                Spacer()
            }.padding([.leading, .trailing], 12)
            .navigationBarTitle("Domains of interest")
    }
}

struct DomainsOfInterestView_Previews: PreviewProvider {
    static var previews: some View {
    DomainsOfInterestView()
    }
}

struct DomainOfInterestElement: View {
    @Binding var isActive: Bool
    var domain: String
    var body: some View {
        Button(action: {
            self.isActive.toggle()
        }) {
            VStack {
                Divider().padding(.bottom, 12)
                HStack {
                    checkBoxView()
                        .frame(width: 36, height: 36)
                    textView().font(.custom("SFProDisplay-Regular", size: 16))
                    Spacer()
                }
            }
        }
    }
    
    func checkBoxView() -> Image {
        switch isActive {
        case true:
            return Image(uiImage: #imageLiteral(resourceName: "check-box-active-2-1")).renderingMode(.original)
        case false:
            return Image(uiImage: #imageLiteral(resourceName: "check-box-active-1")).renderingMode(.original)
        }
    }
    func textView() -> Text {
        switch isActive {
        case true:
            return Text(domain)
                .foregroundColor(.black)
        case false:
            return Text(domain)
                .foregroundColor(Color.orGrayColor)
        }
    }
}

Could anyone help me please? Thanks.

2
Besides the answers below, you might need to be careful with multiple threads. I can't see the code for the getInterestDomains, but it might be calling back in a background thread. Then changing the published var in background thread could cause undefined things to happen (like the view not updating as it should). You might need to dispatch back to the main queue. This is in addition to not initializing your viewModel in your view unless you are using a @StateObject. - Chris Slade

2 Answers

1
votes

DomainsOfInterestView is recreated on every update of its view model. And you initialise the view model property with a new DomainsViewModel instance every time. A new view model will have the domains property set to [InterestDomain]() and self.getDomains will be called again.

First of all, better to inject the viewModel to DomainsOfInterestView in an initiailiser as @youjin wrote in a comment to the previous post. Also, you can use @StateObject instead of @ObservedObject if your minimum deployment target allows that. In this case, the viewModel property won't be reset every time the view is updated.

-1
votes

Quoting the answer from here

If your data can be changed, you need a dynamic ForEach loop (with an explicit id parameter):

 ForEach(viewModel.domains.indices, id: \.self) { index in // <=
    // ...
 }

In some cases Xcode warns you when you try to modify your array used in a ForEach loop:

ForEach(:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(:id:content:) and provide an explicit id!

Tested code


struct DomainsOfInterestView: View {

    @ObservedObject var viewModel: DomainsViewModel = DomainsViewModel()
    
    @State var isActive = true
    
    var body: some View {
            VStack(alignment: .center) {
                HStack {
                    Text("Choose domains of interest for your profile")
                        .font(.headline)
                    Spacer()
                }.padding(.bottom, 16)
                
                ForEach(viewModel.domains.indices, id: \.self) { index in // <=
                    Text(String(index))
                }

                Button(action: {
                }) {
                    Text("Save")
                }.padding(.top, 30)
                    .padding([.leading, .trailing], 30)
                Spacer()
            }.padding([.leading, .trailing], 12)
            .navigationBarTitle("Domains of interest")
    }
}


class DomainsViewModel: ObservableObject {

    @Published var domains: [Int] = [Int]()
    
    init() {
        self.getDomains { (response) in
            print(response)
        }
    }
    
    func getDomains(completion: @escaping (Bool) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.domains = [1,2,3,4,5]
            completion(true)
        }
    }

}

Tested with Xcode 12 beta 6 on iOS 14 beta 6