Sources/SwiftUIX/SwiftUIX.docc/Articles/SwiftUI-View-Storage.md
SwiftUIX provides a ViewStorage property wrapper, which can be used to add non-observable storage capabilities to a view property.
The ViewStorage property wrapper works just like @State, except that modifying a @ViewStorage value does not cause the view's body to update.
View storage does however, tie the lifetime of the value to the lifetime of the view holding the @ViewStorage, just like @State.
@ViewStorageImagine that you have a scrollable view, MyScrollView, that allows the parent to observe its scroll content offset via a Binding, where your use case for observing the value is to hide your app's navigation bar if the content is scrolled beyond a certain threshold.
A simple implementation would be this:
struct MyView: View {
@State private var scrollContentOffset: CGPoint
var body: some View {
MyScrollView(contentOffset: $scrollContentOffset)
.navigationBarHidden(scrollContentOffset.y < 0)
}
}
Now, while this implementation is functional, it is not the most performant. You only care about hiding the navigation bar if the scroll content offset's y-value is below a certain threshold, but because you are updating the scroll content offset in a @State variable, your entire view will refresh everytime the scroll content offset changes (including MyScrollView itself).
Updating a scroll view, especially one that is moving, at a touch response rate of 120hz, is not performant, especially when that update is entirely redundant in the first place.
You could solve this by putting scrollContentOffset in a model object:
struct MyOptimizedView: View {
class ScrollContentOffsetTracker: ObservableObject {
var scrollContentOffset: CGPoint = .zero {
didSet {
isNavigationBarVisible = scrollContentOffset.y < 0
}
}
@Published var isNavigationBarVisible: Bool = false
init() {
...
}
}
@State private var scrollContentOffsetTracker = ScrollContentOffsetTracker()
var body: some View {
MyScrollView(contentOffset: $scrollContentOffsetTracker.scrollContentOffset)
.navigationBarHidden(scrollContentOffsetTracker.isNavigationBarVisible)
}
}
While the implementation has now become more complex, your view now only updates when isNavigationBarVisible updates, which only happens when the scroll content offset's y-value goes below or above a certain threshold. Changing the scrollContentOffset does not trigger an update, since the property is not marked as a @Published variable.
This is where @ViewStorage comes in. Instead of having to implement a custom model class each time you encounter this scenario, @ViewStorage provides a stateful, but non-view-invalidating, means to store a value.
struct MyCleanOptimizedView: View {
@ViewStorage private var scrollContentOffset: CGPoint
@State private var isNavigationBarVisible: Bool = false
var body: some View {
MyScrollView(contentOffset: $scrollContentOffset.binding)
.navigationBarHidden(isNavigationBarVisible)
.onReceive($scrollContentOffset.publisher) { offset in
isNavigationBarVisible = scrollContentOffset.y < 0
}
}
}
In this case, the implementation is still concise, yet functionally equivalent to the the one above. @ViewStorage offers a publisher that allows you track changes to its wrapped value, which paired with SwiftUI's View.onReceive(_:perform) offers a convenient update block where you can perform your logic.