swiftswiftuibindingstate

run when a view redraws in SwiftUI


I have a view in SwiftUI, and I would like it to both redraw and to run a closure whenever a variable in my model changes. I am using this closure to update a state var I am storing in the view, which should be the previous value of the variable in my model before it changes

The following code simulates my situation:

let viewModel = ViewModel()

struct someView: View {

@observedObject var viewModel: ViewModel = viewModel
@State var previousSomeValue: CGFloat = 0 

    var body: some View {
        Text("\(viewModel.model.someValue)")
    }
}

class ViewModel: ObservableObject {

@Published var model = Model()

}

struct model {

    var someValue: CGFloat = 0

}

With this setup, if someValue ever changes, someView redraws, however, I am unable to fire a closure.

//Solutions I have tried:

The main one was to attach onChangeOf(_ (T)->Void) to my view. With .onChangeOf( viewModel.model.someValue ) { _ in //do something } I was able to fire a closure whenever it changed however, by the time it ran, viewModel.model.someValue had already updated to the newValue, and I wasnt able to capture the old one. I read in the documentation that this is by design and that you must capture the thing you want to store the old value of, but I (to my knowledge) am only able to capture self, viewModel, but not viewModel.model.someValue.

.onChangeOf( viewModel.model.someValue ) { [self] newValue in //do something } //works but doesnt capture the var

.onChangeOf( viewModel.model.someValue ) { [viewModel] newValue in //do something } //works but doesnt capture the var

.onChangeOf( viewModel.model.someValue ) { [viewModel.model.someValue] newValue in //do something } //does not compile (  Expected 'weak', 'unowned', or no specifier in capture list )

I have also tried creating a binding in the view such as Binding { gameView.model.someValue } set: { _ in } and having the onChange observer this instead, but even when I capture self, when the closure is called, the old and new values are identicial.

This seems like a common thing to do (detecting external changes and firing a closure), how should I go about it?


Solution

  • If I correctly understood your needs then you should do this not in view but in view model, like

    class ViewModel: ObservableObject {
        var onModelChanged: (_ old: Model, _ new: Model) -> Void
        @Published var model = Model() {
            didSet {
                onModelChanged(oldValue, model)
            }
        }
    
        init(onModelChanged: @escaping (_ old: Model, _ new: Model) -> Void = {_, _ in}) {
            self.onModelChanged = onModelChanged
        }
    }
    

    so instantiating ViewModel you can provide a callback to observe values changed in model and have old and new values, like

    @StateObject var viewModel = ViewModel() {
        print("Old value: \($0.someValue)")
        print("New value: \($1.someValue)")
    }