Memoization in SwiftUI views

I have a view which shows some data based on a complex calculation. Let's say I need to parse some input string and transform it in some way.

private func someComplexCalculation(_ input: String) -> String {
  // ...
}

The most naive approach would be to perform this in the view's body:

struct MyView: View {
  let input: String

  var body: some View {
    Text(someComplexCalculation(input))
  }
}

But of course, we want to keep body nice and fast because SwiftUI may call it very often. The text to be displayed will be constant for a particular view, in a particular place in the hierarchy (barring some major events such as locale changes, which can basically change the entire UI anyway).

So the next idea would be to hoist the calculation out of body in to the view's initialiser:

struct MyView: View {
  let value: String

  init(_ value: String) {
    self.value = someComplexCalculation(value)
  }

  var body: some View {
    Text(value)
  }
}

Except that this view's initialiser will be called in the body of its parent, so this isn't really much of a win at all.


So the next idea is that we need to associate the cached data with the underlying view itself somehow. From what we are told about SwiftUI, that's what @State does.

struct MyView: View {
  @State var value: String

  init(_ value: String) {
    self._value = State(initialValue: someComplexCalculation(value))
  }

  var body: some View {
    Text(value)
  }
}

Except... apparently this is not recommended because SwiftUI won't honour the value set in the initialiser.

That's kind of okay for my purposes - the contents won't change, and every view with the same identity (place in the hierarchy) will be provided the same input. The real problem emerges when we look at the documentation for @State. Its initialiser takes a value directly, so we're still going to perform this expensive calculation every time; we'll just discard the value immediately afterwards and take one which the framework memoised.

Which brings me on to my final approach - @StateObject. Unlike @State, its initialiser takes an autoclosure, so we won't recompute the value every time. But it should still be stored in a way that is bound to the underlying view, thereby giving me a place to stash memoised values.

struct MyView: View {

  final class Cache {
    var transformed: String

    init(input: String) {
      self.transformed = someComplexCalculation(value)
    }
  }

  @StateObject var cache: Cache

  init(_ value: String) {
    self._cache = StateObject(wrappedValue: Cache(input: value))
  }

  var body: some View {
    Text(cache.transformed)
  }
}

I haven't been able to find much in the way of others online using @StateObject for this purpose, so I'd like to ask - is there some other solution I'm overlooking? Is this considered a misuse of @StateObject for some reason? The documentation for the StateObject initialiser says:

Initialize using external data

If the initial state of a state object depends on external data, you can call this initializer directly. However, use caution when doing this, because SwiftUI only initializes the object once during the lifetime of the view — even if you call the state object initializer more than once — which might result in unexpected behavior.

Which seems fine. This seems like exactly what I want.

Using @StateObject for caching computed values in SwiftUI can be a valid approach in certain scenarios. It allows you to associate the cached data with the underlying view and ensures that the cached value persists across view updates.

The approach you've described using @StateObject looks reasonable. You've created a separate Cache class to hold the computed value, and the @StateObject property wrapper ensures that the cache is retained and updated properly.

Regarding the warning in the @StateObject documentation about initializing with external data, it is advising caution because SwiftUI only initializes the state object once during the lifetime of the view. This means that if you call the state object initializer multiple times with different external data, the state object will not be reinitialized. Instead, it will retain the value from the first initialization, which may lead to unexpected behavior if you rely on external data to change the cached value.

However, in your case, you're initializing the @StateObject with the computed value based on the provided input string. As long as the input string remains constant for a particular view, the warning does not apply, and your approach should work as intended.

In summary, using @StateObject to cache computed values in SwiftUI can be a suitable solution, especially when the computation is expensive and you want to avoid recomputing it unnecessarily. Just make sure to consider the behavior of @StateObject with external data initialization and ensure that it aligns with your requirements in terms of view updates and data consistency.

We are actually using this pattern quite a lot in our framework and demo apps. As you may find, computing resource-intensive mapping related objects is time consuming!

We have even written a doc - "Working with Geo Views | ArcGIS Documentation" (albeit concise) to explain the right pattern for using our framework. 😄

And your assessment is correct - State property wrapper and its initializer should not be used with an injected variable - it should only be used with a constant or created within the view. StateObject's initializer can be used, but with caution.

Memoization in SwiftUI views
 
 
Q