Are you having trouble using @SceneStorage to configure a Core Data @FetchRequest? Here’s what worked for me.
Saving View State
I’m using the @SceneStorage property wrapper to save and restore the search/filter state of a list view:
My root view holds both the search scope state and the Core Data fetch request that provides the list of items:
struct WorldView: View {
@SceneStorage("searchScope")
private var scope = SearchScope()
@FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
private var countries: FetchedResults<Country>
var body: some View {
NavigationView {
VStack {
CountryScope(scope: $scope)
.padding()
CountryList(countries: countries)
}
.navigationTitle("World")
NoCountrySelected()
}
.onChange(of: scope) { newScope in
countries.nsPredicate = newScope.predicate
}
}
}
See SceneStorage for custom types for the details on how I made my SearchScope
type work with @SceneStorage. The problem is that, as written, this approach doesn’t work.
What’s The Problem?
The @SceneStorage property wrapper restores the state of my search scope but it’s not triggering a correctly initialized fetch request to update the list of countries. This leaves me with a country list that doesn’t match the restored search scope:
The problem is that the onChange action is not triggered when the SceneStorage property wrapper initializes. That means the predicate is not set correctly on the fetch request for the restored search scope.
Refactoring The Fetch Request
With the benefit of hindsight, there a few things I can improve about this code. The fetch request depends on the search scope but we cannot rely on the onChange action to initialize it. It’s also hard to understand the order in which the system initializes the scene storage and fetch request and how they interact with the on change action.
Refactoring to move the fetch request down into the CountryList
view makes the dependencies and initialization clearer. I’m keeping the search scope storage in the root view and passing the predicate as a dependency. This simplifies my root view and removes the need for the onChange
action:
struct WorldView: View {
@SceneStorage("searchScope")
private var scope = SearchScope()
var body: some View {
NavigationView {
VStack {
CountryScope(scope: $scope)
.padding()
CountryList(predicate: scope.predicate)
}
.navigationTitle("World")
NoCountrySelected()
}
}
}
We now rely on SwiftUI to refresh the CountryList
view when needed. Here’s how my country list view ends up:
struct CountryList: View {
@FetchRequest
private var countries: FetchedResults<Country>
init(predicate: NSPredicate? = nil) {
_countries = FetchRequest(sortDescriptors: [SortDescriptor(\.name)],
predicate: predicate)
}
var body: some View { ... }
}
I’ve added an initializer that takes the predicate and uses it to configure the fetch request. Note that you access the wrapped property by preceding the property name with an underscore (_countries
). This approach now works as expected but I think it’s also easier to understand.