I'm trying to implement a UI for a classic parent-children (master-details, etc.) model. All works fine when I have a direct reference to the children in the view, but when I introduce a view-model for the children the UI stops updating when children are updated. I'm pretty sure that when I introduce the child view model I am creating a copy of the child struct and that is why the UI is not being updated. I suspect there is a common pattern I should be using but have not yet discovered.
I'm using Xcode 13.3 and so, I believe, Swift 5.5.
Here's code I'm using to try to solve the problem. This code does not use a child view model and the UI is correctly updated:
Working Code
Model
import Foundation
struct Parent {
private(set) var children: Array<Child>
private(set) var numberTimes: Int
init(numberOfChildren: Int) {
numberTimes = 0
children = []
for index in 0..<numberOfChildren {
children.append(Child(value: index))
}
toggleRandom()
}
mutating func choose(child: Child) {
if let chosenIndex = children.firstIndex(where: { $0.value == child.value }) {
children[chosenIndex].toggleIsSelected()
}
}
mutating func pressMe() {
numberTimes += 1
}
mutating func toggleRandom() {
children[Int.random(in: 0..<children.count)].toggleIsSelected()
}
}
struct Child: Identifiable {
let id: Int
private(set) var value: Int
private(set) var isSelected = false
init(value: Int) {
self.id = value
self.value = value
}
mutating func toggleIsSelected() {
isSelected.toggle()
}
}
ViewModel with raw children
import SwiftUI
class ParentViewModel: ObservableObject {
@Published var parent: Parent
init(numberOfChildren: Int) {
parent = Parent(numberOfChildren: numberOfChildren)
}
var children: Array<Child> {
parent.children
}
var parentText: String {
"'Press Me' pressed \(parent.numberTimes)"
}
func pressMe() {
parent.pressMe()
}
func toggleRandom() {
parent.toggleRandom()
}
}
View with raw children
import SwiftUI
struct ContentView: View {
@ObservedObject var parentViewModel: ParentViewModel
var body: some View {
VStack {
Text(parentViewModel.parentText).padding()
Button { parentViewModel.pressMe() }
label: { Text("Press Me") }
ForEach(parentViewModel.children, id: \.id) { child in
Text(String(child.value))
.foregroundColor(child.isSelected ? .white : .black)
.background(child.isSelected ? .black : .white)
}
Button { parentViewModel.toggleRandom() }
label: { Text("Toggle Random") }
}
}
}
Here's the code modified to use a view model for the child data elements. The model is the same as above, so only the view model and view code is different. This code continues to update the parent text correctly, but does not update the UI when child values change.
Not Working Code
ViewModel with child view model
import SwiftUI
class ParentViewModel: ObservableObject {
@Published var parent: Parent
private(set) var children: Array<ChildViewModel>
init(numberOfChildren: Int) {
parent = Parent(numberOfChildren: numberOfChildren)
children = []
for child in parent.children {
children.append(ChildViewModel(child: child))
}
}
var parentText: String {
"'Press Me' pressed \(parent.numberTimes)"
}
func pressMe() {
parent.pressMe()
}
func toggleRandom() {
parent.toggleRandom()
}
}
class ChildViewModel: ObservableObject, Identifiable {
@Published var child: Child
init(child: Child) {
self.child = child
}
var value: Int {
child.value
}
var isSelected: Bool {
child.isSelected
}
}
View with child view model
import SwiftUI
struct ContentView: View {
@ObservedObject var parentViewModel: ParentViewModel
var body: some View {
VStack {
Text(parentViewModel.parentText).padding()
Button { parentViewModel.pressMe() }
label: { Text("Press Me") }
ForEach(parentViewModel.children, id: \.id) { child in
ChildView(childViewModel: child)
}
Button { parentViewModel.toggleRandom() }
label: { Text("Toggle Random") }
}
}
}
struct ChildView: View {
@ObservedObject var childViewModel: ChildViewModel
var body: some View {
Text(String(childViewModel.value))
.foregroundColor(childViewModel.isSelected ? .white : .black)
.background(childViewModel.isSelected ? .black : .white)
}
}