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.