Using @SceneStorage With @FetchRequest

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:

List of countries with a filter bar set to All, search field containing Ba and a scope bar set to capital

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:

Search scope is capitals beginning &ldquo;Ba&rdquo; but country list does not match

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.