My app has two modes:
A "run" mode executes very fast without any SwiftUI View update, and a "single step" mode where the user advances the app state manually and needs View updates at every step.
In the single step mode, SwiftUI updates my View as usual. For the run mode, I thought about to disable the automatic View update using a suggestion from this post.
To test the approach, I wrote the following minimal project.
If I did understand the suggestion right, CustomView
should not be updated, since the equality function returns always true
.
But the View is updated and I don't understand why.
EDIT:
My app requires to switch between the run mode and the single step mode at run time. Thus, it is not possible to rebuild the app for the one or the other mode.
@Observable
final class Model {
var counter = 0
var timer = Timer()
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.counter += 1
}
}
}
@main
struct ViewEquatableApp: App {
let model = Model()
var body: some Scene {
WindowGroup {
CustomView(model: model)
.equatable()
}
}
}
struct CustomView: View, Equatable {
let model: Model
static func == (lhs: CustomView, rhs: CustomView) -> Bool {
// << return yes on view properties which identifies that the
// view is equal and should not be refreshed (ie. `body` is not rebuilt)
true // <- true should disable the View update
}
var body: some View {
ContentView(model: model)
}
}
struct ContentView: View {
let model: Model
var body: some View {
Text("Counter: \(model.counter)")
}
}
My next attempt to achieve disabling SwiftUI View updates is the following.
I am learning SwiftUI, and I am sure this is not the best way to implement it. Any suggestion how to do it right is therefore highly welcome!
The approach sketched in the question is, as lorem ipsum in his last comment pointed out, something that worked apparently with older SwiftUI versions but no longer when using the @Observable
macro.
In any case this approach was much too complicated. There is a simpler solution that should work always.
A SwiftUI is normally re-drawn, when an observed property changes. This is normally controlled by a ViewModel, not by the DataModel itself.
I thus changed the code so that the App
creates a ViewModel
that is passed to the ContentView
:
@main
struct MyApp: App {
let viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView(viewModel: viewModel)
}
}
}
ContentView
accesses viewModelCounter
of the passed ViewModel
, and displays it:
struct ContentView: View {
let viewModel: ViewModel
var body: some View {
Text("Counter: \(viewModel.viewModelCounter)")
}
}
ViewModel
is @Observable
. It has a property model
, the data model initialized here, and a property viewModelCounter
that is displayed by the ContentView
.
Modal
has a property mode
that determines if data changes in the model should be forwarded back to ViewModel.viewModelCounter
. If so, changes to ViewModel.viewModelCounter
would let ContentView
to re-draw its contents. Model
has also a property modelCounter
that is incremented repeatedly by a timer, to simulate changing data. Model
has also a reference back to its ViewModel
to be able to change viewModel.viewModelCounter
if requested.
class Model {
var mode = Mode.singleStep
var viewModel: ViewModel?
var modelCounter = 0
var changeDataTimer = Timer()
func changeDataRepeatedly() {
changeDataTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [self] _ in
self.modelCounter += 1
if mode == .singleStep {
viewModel?.viewModelCounter = modelCounter
}
}
}
}
This test app works as required, but, as said before, it surely not the best solution.