Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - ObservableObject performance issues

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.

Screenshot

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?

Edit:

To better illustrate why this can be a problem, suppose ImageView does more than just display a static image. For example, it might:

  • Asynchronously load an image, trigged by a subview's init or onAppear method
  • Contain running animations
  • Support a drag-and-drop interface, requiring local state management

There'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.

like image 424
Hundley Avatar asked Nov 25 '19 20:11

Hundley


1 Answers

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
like image 107
rob mayoff Avatar answered Sep 24 '22 12:09

rob mayoff