0

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)

    }
}
  • You solved it yourself, we don’t use view model objects in SwiftUI. We just have one object, usually an environment object that holds the model structs and then we use @Binding to edit – malhal Apr 08 '22 at 08:09
  • The issue you are having is that you are attempting to use a nested view model. Essentially, `@Published` only updates a `View` struct and not another class. [See this Answer](https://stackoverflow.com/a/58406402/7129318) While this workaround will work, it is cleaner to alter how you handle the view models. – Yrb Apr 08 '22 at 14:01

0 Answers0