Blog post: https://showmax.engineering/articles/unexpected-redrawing-in-swiftui
At first sight, I absolutely loved how simple it was to announce changes to SwiftUI views. You just set a value in @Published
variables and the view gets reloaded with the fresh value. But soon I realized that there was something I’d missed: the context. In fact, it is described in Apple docs. But it's quite easy to overlook all of the consequences when reading. You understand it better in practice. Let’s have a look at a few examples.
@Observable
macro, so SwiftUI will redraw only those parts of UI for whose the model value actually did change, and does not redraw unrelated UI. Consider a model that implements ObservableObject
with several @Published
variables. ObservableObject
will automatically synthesize the objectWillChange
property. Each SwiftUI view that uses your ObservableObject
will subscribe to the objectWillChange
changes. Now, if your model changes one of the @Published
variables, then SwiftUI will redraw all of the views that use your ObservableObject
.
For full details see our Github repo.
class SeriesModel: ObservableObject {
@Published var title: String = "Tali's Wedding"
@Published var isMyFavourite: Bool = false
@Published var episodes: [Episode] = [...]
}
This object is shared in several SwiftUI views where each view reads some @Published variable (either directly or indirectly via Binding).
struct ContentView: View {
@StateObject var model = SeriesModel()
var body: some View {
VStack(spacing: 16) {
TitleView(title: model.title)
MyFavouriteView(model: model)
EpisodesView(model: model)
}
}
}
Here’s a full code example: https://github.com/Showmax/ios-swiftui-redraws
Now, if you tap on the heart button, what views will be redrawn?
To see that, use the hint from Peter Steinberger: add .background(Color.debug)
into each view's body. You can also add Self._printChanges()
.
public extension ShapeStyle where Self == Color {
static var debug: Color {
#if DEBUG
return Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
#else
return Color(.clear)
#endif
}
}
Now set this debug color as the background for each of your views.
TitleView(model: model).background(.debug)
The problem is that all of the views (even “episodes”) got redrawn, despite the fact that we only update the “Add series to favorites” button.
Your gut feeling would tell you, it is…bad. Too many redraws. Fix it.
But first, let’s explore the pros and cons of the current approach.
@Published
variables inside a single ObservableObject which is very easy to understandTo correctly assess whether the extra redraws are a problem or not, ask yourself two additional questions: do you code for older devices, and do you see any issues when profiling the app with Instruments? In our case, the answer to both questions is “no”, so it's not worth it. Just accept some possible extra redraws and have simpler code.
We would especially recommend verifying view redraws if:
If view redraws hurt the UX or cause other problems, what then?
We have tried several of the options below. They are sorted by their complexity. If you have problems with your current setup, go ahead and try the next, more complex approach.
GIF example:
Example3b
... via callback closure, downside is that it involves changes in child objectExample3c
... via publishers available in child object, all observing handled in parent object, no need to change child objectExample3d
... via common external manager object, in case child anyway communicate with some external manager, then also parent can listen to it.GIF example:
GIF example:
At first, extra view updates look dangerous. It's definitely paying off to keep an eye on them. If they don't happen that much, just let them go and prefer simpler, more readable code. You can always optimize when really needed.