How to design an efficient view model for SwiftUI?

665 views Asked by At

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?

1

There are 1 answers

0
malhal On

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)