Taming SwiftUI Redraws with NonReactiveState
Disclaimer: I came up with this technique when working on Weathergraph (specifically managing the internal state of a component with multiple drag gestures), but have asked GPT4 to write this blog post.
Hey there! I stumbled upon a neat little trick in SwiftUI that I think could save a lot of us from the headache of unnecessary view redraws. It’s called NonReactiveState
, and it's a smart way to keep some of your view's state from causing constant updates. Let's dive in and see how it works, and where it shines the most.
What’s NonReactiveState?
NonReactiveState
is a property wrapper designed for those pieces of state in your SwiftUI views that you don't want to trigger a redraw every time they change. Think of it as a little hideout where you can keep state changes that the SwiftUI update system (let's call it the redraw police) doesn't need to know about.
Here’s the gist of it:
public class NonReactiveState<T> {
private var value: T
public init(wrappedValue: T) {
self.value = wrappedValue
}
public var wrappedValue: T {
get { return value }
set { value = newValue }
}
}
A Practical Example
Imagine a scenario where you’re tracking the start time of a user’s gesture, but updating this state shouldn’t necessarily cause your view to redraw. Using a regular @State
for this could lead to unnecessary performance hits or, worse, redraw loops in complex scenarios.
struct GestureTrackerView: View {
@State private var gestureCount = 0
@State private var lastGestureStartDate = NonReactiveState(wrappedValue: Date())
var body: some View {
VStack {
Text("Gesture count: \(gestureCount)")
Circle()
.gesture(
DragGesture().onChanged { _ in
// This won't cause a redraw
self.lastGestureStartDate.wrappedValue = Date()
}.onEnded { _ in
// This will, updating the gesture count
self.gestureCount += 1
}
)
}
}
}
In this example, updating lastGestureStartDate
doesn't trigger a redraw, but changing gestureCount
does. This way, we keep our UI performance smooth and only refresh the view when actually needed.
When to Use (and Not Use) NonReactiveState
Do use NonReactiveState
when:
- You’re tracking internal state changes that don’t impact the UI directly.
- You want to avoid redraw loops in complex view hierarchies.
- You’re dealing with performance-sensitive scenarios where minimizing redraws is crucial.
Avoid using NonReactiveState
when:
- The state change directly impacts what’s displayed on the screen.
- You’re unsure whether a state change should trigger a redraw (better safe than sorry).
Why It Works
NonReactiveState
works by simply keeping SwiftUI's redraw police in the dark about certain state changes. Since the state wrapped in NonReactiveState
doesn't trigger the update system when it changes, you effectively get a way to manage "silent" state changes—perfect for all those internal flags and counters.
This trick is especially handy in situations where a state change could easily trigger an infinite loop of redraws. By selectively deciding which state changes should trigger updates, you maintain control over your view’s performance and behavior.
Wrapping Up
NonReactiveState
is a powerful tool in your SwiftUI toolbox, especially when you're looking to fine-tune your app's performance. Remember, with great power comes great responsibility—use it wisely to keep your SwiftUI views efficient and responsive.
That’s it for today! Keep experimenting and finding ways to make your SwiftUI apps better. Happy coding!
Just for comparison sake, here’s Claude 3 Opus version:
Efficient State Management in SwiftUI with NonReactiveState
SwiftUI’s declarative nature is powerful, but sometimes we need to manage state without triggering view updates. Enter NonReactiveState
, a handy property wrapper that allows you to store mutable data without causing expensive re-renders.
Here’s how it works:
public class NonReactiveState<T> {
private var value: T
public init(wrappedValue: T) {
self.value = wrappedValue
}
public var wrappedValue: T {
get { value }
set { value = newValue }
}
}
NonReactiveState
is a generic class that wraps a value of type T
. It needs to be stored as a @State
property because regular var
or let
properties are reinstantiated when the view is redrawn.
Here’s an example where NonReactiveState
shines:
struct ContentView: View {
@State private var lastGestureStartDate: NonReactiveState<Date?> = .init(wrappedValue: nil)
var body: some View {
Text("Drag me!")
.gesture(
DragGesture()
.onChanged { _ in
lastGestureStartDate.wrappedValue = Date()
}
.onEnded { _ in
if let startDate = lastGestureStartDate.wrappedValue,
Date().timeIntervalSince(startDate) > 1 {
print("Long drag detected!")
}
}
)
}
}
In this example, we use NonReactiveState
to store the start date of a drag gesture. Updating this value in the onChanged
and onEnded
closures doesn't trigger a view update, preventing unnecessary re-renders.
When to use NonReactiveState
:
- For bookkeeping flags or intermediate values that don’t affect the view’s appearance
- When updating state frequently (e.g., during gestures or animations) to avoid performance issues
- To prevent infinite loops caused by state changes triggering re-renders, which in turn update the state
When not to use NonReactiveState
:
- For data that directly impacts the view’s appearance, as changes won’t be reflected
- When you need to bind the state to other views or controls
Under the hood, NonReactiveState
works by hiding state changes from SwiftUI's update system. This allows you to manage internal data without triggering the usual view update cycle.
Give NonReactiveState
a try in your SwiftUI projects to optimize performance and avoid unnecessary re-renders. Just remember to use it judiciously and only for data that doesn't directly affect your views!
Happy coding!