Taming SwiftUI Redraws with NonReactiveState

Tomáš Kafka
4 min readMar 7, 2024

--

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!

--

--