Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to trigger automatic SwiftUI Updates with @ObservedObject using MVVM

I have a question regarding the combination of SwiftUI and MVVM.

Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)

So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.

View:


struct ContentView: View {
    
    @ObservedObject var viewModel = ViewModel()
    @ObservedObject var numberStorage = NumberStorage()
    
    var body: some View {
        VStack {
//            Text("\(viewModel.getNumberObject().number)")
//                .padding()
//            Button("IncreaseNumber") {
//                viewModel.increaseNumber()
//            }
            Text("\(numberStorage.getNumberObject().number)")
                .padding()
            Button("IncreaseNumber") {
                numberStorage.increaseNumber()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ViewModel:


class ViewModel: ObservableObject {
    
    @Published var number: NumberStorage
    
    init() {
        self.number = NumberStorage()
    }
    
    func increaseNumber() {
        self.number.increaseNumber()
    }
    
    func getNumberObject() -> NumberObject {
        self.number.getNumberObject()
    }
    
} 

Model:


class NumberStorage:ObservableObject {
    @Published var numberObject: NumberObject
    
    init() {
        numberObject = NumberObject()
    }
    
    public func getNumberObject() -> NumberObject {
        return self.numberObject
    }
    
    public func increaseNumber() {
        self.numberObject.number+=1
    }
}

struct NumberObject: Identifiable {
    let id = UUID()
    var number = 0
} ```

Looking forward to your feedback!
like image 290
Jonas Bäumer Avatar asked Jan 24 '26 23:01

Jonas Bäumer


2 Answers

I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:

  1. Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
  2. Events that the view may produce (in your case, a button tap) Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that: View:

struct ContentView: View {
    
    @StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
    private var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
                .padding()
            Button("IncreaseNumber") {
                viewModel.increaseNumber()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ViewModel:


class ViewModel: ObservableObject {
    
    @Published
    private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
    private let numberStorage = NumberStorage()
    
    init() {
        numberStorage.currentNumber
            .map { $0.number }
        .assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
    }
    
    func increaseNumber() {
        self.numberStorage.increaseNumber()
    }
}

Model:

class NumberStorage {
    private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())

    var currentNumber: AnyPublisher<NumberObject, Never> {
        currentNumberSubject.eraseToAnyPublisher()
    }
    
   func increaseNumber() {
       let currentNumber = currentNumberSubject.value.number
       currentNumberSubject.send(.init(number: currentNumber + 1))
    }
}


struct NumberObject: Identifiable { // I'd not use this, just send and int directly
    let id = UUID()
    var number = 0
}
like image 56
Pastre Avatar answered Jan 26 '26 12:01

Pastre


It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.

To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:

class ViewModel: ObservableObject {
    init() {
        number.objectWillChange.sink { [weak self] (_) in
            self?.objectWillChange.send()
        }.store(in: &cancellables)
    }
}

You better listen to only the object you care not the whole observable class if it is not needed.

Plus:

Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app

like image 32
ibrahimyilmaz Avatar answered Jan 26 '26 12:01

ibrahimyilmaz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!