I’ve been chipping away at an update for my app, Peak, which makes a bunch of things in the app significantly faster.

Here, for instance, is how the Recaps screen in Peak loaded before the update. I’m opening my recap for 2024, and once that’s loaded, switching over to 2023:

And here’s what it looks like after the update:

That’s a lot faster! But it’s also a bit janky.

The problem is straightforward. When the app doesn’t have a value to display for the current period, it reverts to showing some placeholder data.

This works fine when the loading takes a while, as it did before the update. But when the data loads fast enough, it creates this jarring flicker, taking you from the previous value to the placeholder and then the new value, all in the span of a few frames.

My first instinct was to solve this in the model. I could track the last non-nil value and display it until the new data arrives. But in practice, that got messy fast.

It made my previous simple models a lot more complex, and this logic would need to be added to every single view model or TCA Reducer where I wanted the behavior as well. But at its core, the biggest issue was this was pushing a bunch of presentation layer work into the model.

So instead I came up with a way to handle this in the view itself. Here’s the creatively named SwiftUI helper view I came up with:

import SwiftUI

struct CachingLastNonNilValue<Value: Equatable, Content: View>: View {
	var value: Value?
	var placeholder: Value
	var timeout: Duration?
	
	@State private var lastNonNilValue: Value?
	
	@ViewBuilder var content: (Value) -> Content
	
	var body: some View {
		content(
			value
			?? lastNonNilValue
			?? placeholder
		)
		.onChange(of: value, initial: true) { _, value in
			guard let value else { return }
			lastNonNilValue = value
		}
		.task(id: value == nil) {
			do {
				guard value == nil, let timeout else { return }
				try await Task.sleep(for: timeout)
				lastNonNilValue = nil
			} catch {
				print("Task was cancelled")
			}
		}
	}
}

The view accepts an optional value as well as a placeholder. As your value updates, it keeps track of the last non-nil value, and uses it to power your content when the value changes to nil.

It also lets you specify an optional timeout, which clears out the last non-nil value stored and reverts to the placeholder after a certain duration has passed, which can be useful if the loading process takes an unexpectedly long time.

Here’s what it looks like at the call site:

// Before
RecapSummary(
	metric: metric,
	recap: store.recaps?[metric] ?? .placeholder(forMetric: metric)
)

// After
CachingLastNonNilValue(
	value: store.recaps?[metric],
	placeholder: .placeholder(forMetric: metric),
	timeout: .seconds(0.3)
) { recap in
	RecapSummary(metric: metric, recap: recap)
}

With this view in place, here’s what the final shipping UI looks like:

Now that’s much better!

It’s being able to create abstractions like this that I love most about SwiftUI. This little 30 line helper can be plugged into any view in my app, and makes so much UI and model code simpler and easier to reason about.