4

I have the following view model:

class ViewModel: ObservableObject {
    @Published var brightnessValue: Double = 0.0
    @Published var saturationValue: Double = 0.0
    @Published var contrastValue:   Double = 0.0
}

In the UI layer I then have 3 sliders, implemented as SwiftUI views, each bound to a Double from the above view model. Whenever a slider changes a property, all 3 sliders redraw. That's because a change to a Published property in an ObservableObject redraws all the views that reference it.

This is how the SwiftUI views looks like:

struct RootView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            AdjustmentSlider(name: "brightness", sliderValue: $viewModel.brightnessValue)
            AdjustmentSlider(name: "saturation", sliderValue: $viewModel.saturationValue)
            AdjustmentSlider(name: "contrast",   sliderValue: $viewModel.contrastValue)            
        }
        .background(.random)
    }
}

struct AdjustmentSlider: View {
    let name: String
    @Binding var sliderValue: Double

    var body: some View {
        Slider(value: $sliderValue)
            .background(.randomColor) // I use this to visualize the redrawing.
    }
}

Finally, this is the overall (sample) app design:

enter image description here

This is will be part of a very complex app, with dozens of sliders, so I'm afraid this constant redrawing could become a non-trivial performance bottleneck as the view model becomes more complex.

Any suggestions on how to design a more efficient view model (i.e., less redraws) for SwiftUI? And, more broadly, is this even a valid concern when using SwiftUI circa iOS 16?

3
  • Redraw != recalculation. In general, though, you can store a view model at the top of your tree. Then, quickly distribute only the info the views need to specific views. In the code you provided, there's little opportunity to do this, although on a small scale, each slider only gets the particular value it needs. Commented Feb 28, 2023 at 21:59
  • There is no such thing as an efficient reference view model, reference types cause more redrawing than value types and the higher up it lives the more redrawing it causes as you are experiencing, you can easily make this a struct and get better performance. Commented Feb 28, 2023 at 23:08
  • 1
    Do you need a value of the slider to be published every time user changes it, or only when user is done editing? If you need it published as user is drugging it, then your design is fine. If you need it only when user is done, keep the "current value" as a private @State inside the slider, and only change an external @Binding in onEditingChanged(false). I don't think it would affect a performance, but it creates a better boundary between "internal state of the UI component" and a "value user selected", and less times model needs to change Commented Mar 1, 2023 at 0:48

1 Answer 1

0

In SwiftUI the View struct is a view model and holds the view data. So using an object for it instead is already inefficient and will lead to consistency bugs. We are supposed to use let for data that doesn't change, @State for data that does change, and @State var struct to group related vars together and you can use mutating func for any logic you'd like to test independently. Use computed var to transform data as you pass it into subview inits in body, e.g. MySubView(myComputedVar), that is the point of the View struct hierarchy - to transform from rich model types to simple types on the way down. In your case it should be like this:

struct MyColor {
    var brightnessValue: Double = 0.0
    var saturationValue: Double = 0.0
    var contrastValue:   Double = 0.0
}

struct RootView: View {
    @State var color = MyColor()

    var body: some View {
        VStack {
            AdjustmentSlider(name: "brightness", sliderValue: $color.brightnessValue)
            AdjustmentSlider(name: "saturation", sliderValue: $color.saturationValue)
            AdjustmentSlider(name: "contrast",   sliderValue: $color.contrastValue)            
        }
        .background(.random)
    }
}

View structs do not "draw" anything. These are super-fast lightweight value types, like int x = 3. It is basically negligible to generate the View struct because it is stored on the memory stack not the heap. SwiftUI recomputes the parts of View hierarchy where it detected a data dependency change (it records what Views call the @State getters), it diffs the hierarchy from last time and it uses the result on that to add/remove/update UIViewController and UIView objects automatically for us. So basically we don't need to worry about View init and body called often, but we need to use value types effectively like @State and @Binding and try not to use objects and never init an object inside of a View struct, since the struct is constantly recreated, it can't hang on to an object - thus doing that is essentially is a memory leak and will slow it down constantly doing pointless heap allocations.

To make SwiftUI more efficient, just break everything up into Views that are as small as possible and where the body only uses the lets/vars that are defined in that View. That is called having "more tightly scoped invalidation" (Data essentials in SwiftUI WWDC 2020 @ 12:21)

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.