I manage the model as an array in viewModel and change the model's properties.
I don't know why the view doesn't change as the model's properties change.
I would appreciate your advice and feedback.
Here is the code:
model
class RoutineUnit: Identifiable, Equatable {
static func == (lhs: RoutineUnit, rhs: RoutineUnit) -> Bool {
return lhs.id == rhs.id
}
let id: String
var title: String
var isSelected: Bool
var targetTask: RoutineUnitTask
var tags: [RoutineUnitTag?]
var tipComment: String
}
viewModel
class RoutineDetailViewModel: ObservableObject {
@Published var routineUnits: [RoutineUnit]
func toggleSelection(for unitID: String) {
if let index = routineUnits.firstIndex(where: { $0.id == unitID }) {
routineUnits[index].isSelected.toggle()
}
}
}
view
struct RoutineUnitView: View {
@ObservedObject var viewModel: RoutineDetailViewModel
var unitID: String
var body: some View {
if let routineUnit = viewModel.getRoutineUnitByID(unitID) {
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.overlay {
...
}
.frame(height: 84)
.onTapGesture {
withAnimation(.spring) {
if(viewModel.isEditingEnabled) {
viewModel.toggleSelection(for: unitID)
}
}
}
}
}
}
viewModel.toggleSelection(for: unitID)Even though it is obviously running, the view is not redrawn. Maybe I don't understand mvvm?
Maybe I don't understand mvvm?
Your misunderstanding is not with MVVM, but how SwiftUI and state changes work.
All a Published property wrapper does is call the objectWillChange publisher of an ObservedObject in the willSet
Handler.
This is all well documented by Apple. I recommend watching the related WWDC videos like Demystify SwiftUI.
Changing the property of an element in a Swift array does not call the willSet
handler of the array itself.
In your case changing the isSelected
property of a RoutineUnit
does not change the routineUnits
array or your RoutineDetailViewModel
.
And since the willSet
handler is not called, no event is emitted by the objectWillChange
publisher and no SwiftUI re-layout / state update is triggered.
In other words, with a Published
property wrapper you do not automatically get a “deep observation” of a property. Of course, this also applies to collection properties and their elements.
There are several ways to achieve what you want.
If your app does not require support for iOS versions prior to iOS 17, you can migrate to the Observable macro, which supports collections and the kind of deep observation you're trying to use.
Otherwise you will have to change your state handling or your data/view modeling.
You could also manually trigger the objectWillChange
publisher in your toggleSelection
method, but that would not be an ideal solution.
If you want changes to a RoutineUnit
model in SwiftUI to result in a state change, they themselves must fulfill the ObservableObject
protocol and you should have a separate view for a single RoutineUnit
object.
Something like this:
final class RoutineUnit: Identifiable, Equatable, ObservableObject {
// ...
}
struct RoutineUnitView: View {
@ObservedObject var routineUnit: RoutineUnit
var body: some View {
VStack {
Text("title: \(routineUnit.title)")
if routineUnit.isSelected {
// ...
}
}
}
}
struct RoutineUnitListView: View {
@ObservedObject var viewModel: RoutineDetailViewModel
var body: some View {
VStack {
ForEach(viewModel.routineUnits) { unit in
RoutineUnitView(routineUnit: unit)
}
}
}
}
I hope you understand the idea, i.e. one view/model for the list, one view/model for each detailed view.
This allows you to change the list as a whole and also the individual elements of the collection.