0

I'm doing a page which consists of 5 Options with radio buttons. When we tap on an option the colour of the element should change. I'm fetching those options from API response. I'm using the MVVM model. I am attaching the code below.

What I'm struggling with is when I change a boolean value, it is not changing!!!

View Model code

import Foundation
import Combine

class DCListViewModel: ObservableObject {
    @Published var DCList = [DCViewModel]()

    init() {
        fetchDcs()
    }

    func fetchDcs() {
        ARMServices().getAllDc { (dcArr) in
            if let dcArr = dcArr {
                for dc in dcArr{
                    self.DCList.append(DCViewModel(dc: dc, isSelected: false))
                }
            }
        }
    }
}


class DCViewModel {
    var id = UUID()
    var dcStruct: DC
    var isSelected: Bool

    init(dc: DC, isSelected: Bool) {
        self.dcStruct = dc
        self.isSelected = isSelected
    }

    var url: String {
        return self.dcStruct.url  ?? "empty URL"
    }

    var dc: String {
        return self.dcStruct.dc ?? "empty dc name"
    }
}

View Page Code

struct SelectTableView: View {

    var body: some View {
        NavigationView {

                VStack {
                    DCListView(dcList: self.dcListVM.DCList)
                        .padding(.horizontal)
                }
            .navigationBarTitle("Select the Data Centre")
        }
    }
}


struct DCListView: View{
    var dcList: [DCViewModel]

    init(dcList: [DCViewModel]) {
        self.dcList = dcList
    }

    var body: some View {
        ForEach(self.dcList, id: \.id) { dc in
            Button(action: {
                print("Tapped")
                dc.isSelected.toggle()
            }){
                ZStack {
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(dc.isSelected ? Color.init("borderSelected"): Color.init("border"))
                        .frame(height: 56)
                        .foregroundColor(.clear)

                    HStack {
                        Text(dc.dc)
                            .font(.custom("Montserrat", size: 16))
                            .fontWeight(.medium)
                            .foregroundColor(dc.isSelected ? Color.init("borderSelected") : .white)
                            .padding()

                        Spacer()

                        ZStack {
                            Circle()
                                .stroke(dc.isSelected ? Color.init("borderSelected") : Color("circleBorder"))
                                .frame(width: 18, height: 18)
                                .padding()

                            Circle()
                                .frame(width: 10, height: 10)
                                .foregroundColor(dc.isSelected ? Color.init("borderSelected"): Color.clear)
                        }
                    }
                }
            }
        }
    }
}

The page looks like this

Azhagusundaram Tamil
  • 2,053
  • 3
  • 21
  • 36

3 Answers3

4

Solution

Change DCViewModel to struct.

Or if you have to keep it as a class, make DCViewModel conform to ObservableObject , and keep a single reference in every singleView. (which means you should subtract that row as a single View)


Explanation

@Published is a property wrapper that publishes change message only when the wrapped value has been set.

The difference between value type and reference type: when you change a variable that holds a value type(struct), it has been set a new value actually. But when you change the value of a reference type(class), the variable that points to that object won't change.

That's why, in your case, when you add a new DCViewModel to the Array(value type), you are always setting a new value to that array, so @Published takes effect. However, the wrapped value is DCViewModel, which is a class(reference type). When you change DCViewModel's value, the wrapped value's setter will not be called and @Published won't publish messages.

David
  • 3,285
  • 1
  • 37
  • 54
ribilynn
  • 460
  • 5
  • 21
3

There are two options.

  1. Change DCViewModel class to struct.
  2. Manually make DCListViewModel class to publish.

See @kontiki 's answer

Change DCViewModel class to struct.

struct DCViewModel {
    var id: UUID
    var dcStruct: DC
    var isSelected: Bool

    init(dc: DC, isSelected: Bool) {
        self.id = UUID()
        self.dcStruct = dc
        self.isSelected = isSelected
    }

    var url: String {
        return self.dcStruct.url  ?? "empty URL"
    }

    var dc: String {
        return self.dcStruct.dc ?? "empty dc name"
    }
}

struct SelectTableView: View {
    // dcListVM missed
    // I assume superview of SelectTableView exist.
    @ObservedObject var dcListVM: DCListViewModel

    var body: some View {
        NavigationView {
                VStack {
                    DCListView(dcList: $dcListVM.DCList)
                        .padding(.horizontal)
                }
            .navigationBarTitle("Select the Data Centre")
        }
    }
}

struct DCListView: View{
    @Binding var dcList: [DCViewModel] // Use @Binding for two-way binding

    var body: some View {
        // You can't use `ForEach(data: self.dcList, id: \.id ) { dc in`
        // Because `dc` is immutable

        ForEach(0..<self.dcList.count) { dc in
            Button(action: {
                print("Tapped")
                self.dcList[dc].isSelected.toggle()
            }){
                ZStack {
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(self.dcList[dc].isSelected ? Color.red: Color.black)
                        .frame(height: 56)
                        .foregroundColor(.clear)

                    HStack {
                        Text(self.dcList[dc].dc)
                            .font(.system(size: 16))
                            .fontWeight(.medium)
                            .foregroundColor(self.dcList[dc].isSelected ? Color.red : Color.black)
                            .padding()

                        Spacer()

                        ZStack {
                            Circle()
                                .stroke(self.dcList[dc].isSelected ? Color.red : Color.black)
                                .frame(width: 18, height: 18)
                                .padding()

                            Circle()
                                .frame(width: 10, height: 10)
                                .foregroundColor(self.dcList[dc].isSelected ? Color.red: Color.clear)
                        }
                    }
                }
            }
        }
    }
}

Manually make instance of DCListViewModel to publish.

struct SelectTableView: View {
    // dcListVM missed
    // I assume superview of SelectTableView exist.
    var dcListVM: DCListViewModel()

    var body: some View {
        NavigationView {

                VStack {
                    DCListView(dcList: self.dcListVM) // DCListViewModel instead of [DCViewModel]
                        .padding(.horizontal)
                }
            .navigationBarTitle("Select the Data Centre")
        }
    }
}


struct DCListView: View {
    @ObservedObject var dcListVM: DCViewModel // DCListViewModel instead of [DCViewModel]

    // See "Memberwise Initializers for Structure Types" in docs.swift.org
    // init(dcListVM: DCViewModel) {
    //     self.dcList = dcList
    // }

    var body: some View {
        ForEach(self.dcList.DCList, id: \.id) { dc in
            Button(action: {
                print("Tapped")
                dc.isSelected.toggle()
                self.dcList.objectWillChange.send()  // Manually make dcList to publish
            }){
                ZStack {
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(dc.isSelected ? Color.init("borderSelected"): Color.init("border"))
                        .frame(height: 56)
                        .foregroundColor(.clear)

                    HStack {
                        Text(dc.dc)
                            .font(.custom("Montserrat", size: 16))
                            .fontWeight(.medium)
                            .foregroundColor(dc.isSelected ? Color.init("borderSelected") : .white)
                            .padding()

                        Spacer()

                        ZStack {
                            Circle()
                                .stroke(dc.isSelected ? Color.init("borderSelected") : Color("circleBorder"))
                                .frame(width: 18, height: 18)
                                .padding()

                            Circle()
                                .frame(width: 10, height: 10)
                                .foregroundColor(dc.isSelected ? Color.init("borderSelected"): Color.clear)
                        }
                    }
                }
            }
        }
    }
}
Makeeyaf
  • 96
  • 2
0

you are just "giving" the copy of dclist to your views, so the view does not know what changes happens. Add @binding to your views to your variable dclist.

Chris
  • 7,579
  • 3
  • 18
  • 38