'View is not updated when the data is changed with ForEach
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.
Solution 1:[1]
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
//....
}
Solution 2:[2]
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).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | HunterLion |
