skip to Main Content

I want to change rate parameter and display in ForEach.

// ViewModel.swift

@MainActor final class ViewModel: ObservableObject {
    let serviceContainer: ServiceContainerProtocol

    @Published var funds: [FundModel] = []
    ...

    init(serviceContainer: ServiceContainerProtocol) {
        self.serviceContainer = serviceContainer

        Task { await getFunds() }
        ...
    }

    ...
}
// FundView.swift

struct FundView: View {
    @StateObject private var viewModel: ViewModel

    init(serviceContainer: ServiceContainerProtocol) {
        self._viewModel = StateObject(wrappedValue: ViewModel(serviceContainer: serviceContainer))
    }

    var body: some View {
        ScrollView { 
            VStack {
                ForEach(viewModel.funds, id: .fundCode) { fund in
                    VStack {
                        Text(String(fund.rate))

                        Button("Add 5") {
                            if let index = viewModel.funds.firstIndex(where: { $0.fundCode == fund.fundCode }) {
                                viewModel.funds[index].rate += 5
                            }
                        }
                    }
                }
            }
        }
    }
}

If I use Struct for model, view did updated as expected.

// FundModel.swift

struct FundModel: Decodable {
    let fundCode: String
    ...

    // Internal - Not related to api.
    var rate: Int = 0
    ...

    // MARK: CodingKeys

    private enum CodingKeys: String, CodingKey {
        case fundCode
        ...
    }

    // MARK: Decodable

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        fundCode = try container.decode(String.self, forKey: .fundCode)
        ...
    }
}

If I use Class for model, view did not updated.

// FundModel.swift

final class FundModel: Decodable {
    let fundCode: String
    ...

    // Internal - Not related to api.
    var rate: Int = 0
    ...

    // MARK: CodingKeys

    private enum CodingKeys: String, CodingKey {
        case fundCode
        ...
    }

    // MARK: Decodable

    public required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        fundCode = try container.decode(String.self, forKey: .fundCode)
        ...
    }
}

I want to use Class for model because it needs to be inherit some properties from some Super Class.

What is the difference between Struct model and Class model for SwiftUI perspective and why view did not update when I use Class model over Struct model?

Note: FundModel conforms Equtable and Hashable somewhere else.

2

Answers


  1. The view updates when the @Published var changes. Your @Published var is an array of FundModel.

    A struct is not a mutable entity, so when you use a struct, the array actually replaces the object with a new one. The system recognises the change in the array and publishes it to the view.

    A class is mutable, so when you change the rate inside the class, you still have the same instance. So, the array of FundModel does not change: it still contains exactly the same elements inside, even though one of the elements have changed an internal variable.

    The @Published wrapper cannot detect that an internal variable of the members of the array has changed: it detects only when the array has different elements inside.

    Conclusion: struct needs to create a new element to change, @Published recognises this. class can change, so for @Published the object is still the same (the array didn’t change).

    Login or Signup to reply.
  2. try this approach using class ViewModel: ObservableObject {...} (without the @MainActor) and viewModel.objectWillChange.send()
    as in this example code:

     struct FundView: View {
        @StateObject private var viewModel: ViewModel
    
        init(serviceContainer: ServiceContainerProtocol) {
            self._viewModel = StateObject(wrappedValue: ViewModel(serviceContainer: serviceContainer))
        }
    
        var body: some View {
            ScrollView {
                VStack {
                    ForEach(viewModel.funds, id: .fundCode) { fund in
                        VStack {
                            Text(String(fund.rate))
    
                            Button("Add 5") {
                                if let index = viewModel.funds.firstIndex(where: { $0.fundCode == fund.fundCode }) {
                                    viewModel.objectWillChange.send() // <-- here
                                    viewModel.funds[index].rate += 5
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    final class FundModel: Decodable {
        let fundCode: String
        var rate: Int = 0
    
       //....
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search