When a SwiftUI View
binds to an ObservableObject
, the view is automatically reloaded when any change occurs within the observed object - regardless of whether the change directly affects the view.
This seems to cause big performance issues for non-trivial apps. See this simple example:
// Our observed model
class User: ObservableObject {
@Published var name = "Bob"
@Published var imageResource = "IMAGE_RESOURCE"
}
// Name view
struct NameView: View {
@EnvironmentObject var user: User
var body: some View {
print("Redrawing name")
return TextField("Name", text: $user.name)
}
}
// Image view - elsewhere in the app
struct ImageView: View {
@EnvironmentObject var user: User
var body: some View {
print("Redrawing image")
return Image(user.imageResource)
}
}
Here we have two unrelated views, residing in different parts of the app. They both observe changes to a shared User
supplied by the environment. NameView
allows you to edit User
's name via a TextField. ImageView
displays the user's profile image.
The problem: With each keystroke inside NameView
, all views observing this User
are forced to reload their entire body content. This includes ImageView
, which might involve some expensive operations - like downloading/resizing a large image.
This can easily be proven in the example above, because "Redrawing name"
and "Redrawing image"
are logged each time you enter a new character in the TextField.
The question: How can we improve our usage of Observable/Environment objects, to avoid unnecessary redrawing of views? Is there a better way to structure our data models?
To better illustrate why this can be a problem, suppose ImageView
does more than just display a static image. For example, it might:
init
or onAppear
methodThere's plenty more examples, but these are what I've encountered in my current project. In each of these cases, the view's body
being recomputed results in discarded state and some expensive operations being cancelled/restarted.
Not to say this is a "bug" in SwiftUI - but if there's a better way to architect our apps, I have yet to see it mentioned by Apple or any tutorials. Most examples seem to favor liberal usage of EnvironmentObject without addressing the side effects.
Why does ImageView
need the entire User
object?
Answer: it doesn't.
Change it to take only what it needs:
struct ImageView: View {
var imageName: String
var body: some View {
print("Redrawing image")
return Image(imageName)
}
}
struct ContentView: View {
@EnvironmentObject var user: User
var body: some View {
VStack {
NameView()
ImageView(imageName: user.imageResource)
}
}
}
Output as I tap keyboard keys:
Redrawing name
Redrawing image
Redrawing name
Redrawing name
Redrawing name
Redrawing name
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With