Core Data and SwiftUI

In the previous post, I shared how you can create custom property wrappers that will work with SwiftUI’s view updating mechanism. I wrote that because I’ve got one other neat property wrapper to share, but understanding how it works requires knowing how to make custom wrappers. Now that I’ve got that out of the way…

Core Data

I really like Core Data. It’s an incredibly full-featured object persistence layer with an enormous number of really cool features. I know that it tends to get a bad rap amongst developers, but as I outlined in my post on the “Laws” of Core Data, I believe that most of the negative press comes from mis-using the framework.

I’m not saying that Core Data is without flaw; far from it. There are definitely changes I’d love to see in the API (a few of which I touch on in that post). But I do not believe it deserves the derision I so often hear from developers.

So when it comes to storing offline data in an app, my natural inclination is to use Core Data for the persistence layer. It’s pretty straight-forward to describe a schema in the editor and use the code generation features of Xcode to get it up and running fairly quickly.

Where I hesitate is introducing Core Data into the view layer. Despite the existence of things like @FetchRequest, I believe that it’s good to keep the layers of my personal apps separate. Core Data, dealing so closely with data organization and persistence, belongs at one of the lowest levels of my apps, and not in the UI.

This is a large part of why I suggest creating an abstraction layer for Core Data. An abstraction layer allows me to hide the nitty gritty details of data mutation and persistence from the parts of the app that deal with displaying that data in the UI.

An initial approach might suggest that creating a Core Data abstraction layer requires foregoing all the nice affordances we have for quickly and expressively extracting information from the persistent store. Not so! The setup we’ll be going over allows me to have the abstraction layer while still using cool things (like property wrappers) for retrieving data.

The Roadmap

When designing a new API, I often like to start with the final syntax of how I want to use a thing, and then work backwards to make that syntax possible. As I was imagining a data abstraction layer, I came up with this:

struct ContactList: View {
    @Query(.all) var contacts: QueryResults<Contact>

    var body: some View {
        List(contacts) { contact in
            Text(contact.givenName)
        }
    }
}

This is of course heavily inspired by @FetchRequest, but that’s not a bad thing. It’s pretty minimal: I define a query to fetch everything (.all the contacts), and I’ve got a basic body implementation. Like with @FetchRequest, I do need a special “results” type, which we’ll get to later.

We’re going to end up with a few different things to make this possible, which I’ll outline here:

  • a DataStore class
  • a Queryable protocol
  • a QueryFilter protocol
  • a Query property wrapper
  • a QueryResults struct

The DataStore

First up is the DataStore. In my post on Core Data, I pointed out that it’s not necessary to create a “stack” object, since an NSManagedObjectContext has everything you need to get and manipulate the underlying objects. So, it might seem odd that the very first thing we’re going to do is wrap up the Core Data stack.

If you think about it though, it makes sense in this situation. We want to provide an abstraction layer on top of Core Data. The entire point is to hide Core Data from the rest of the app, and that implies hiding the persistence controllers from the rest of the app as well (the context, the store coordinator, etc). Thus, I create a DataStore object that encapsulates this. It manages the details of loading the persistent store and, as necessary, modifying the objects therein.

All mutations to the objects happen via the DataStore. I’ve come to really like the “one-way data flow” principle, where data always flows in a single direction. SwiftUI itself largely operates on this principle: as the state changes, the new UI description “flows” out of the new state. When it comes to my model layer, I do the same: the model objects flow out of the DataStore, and if something needs to change, I send a command to the DataStore instructing it to make the change, and new/modified values flow out of it.

The Protocols

The next two bits are closely related, so I’ll lump them together: The Queryable and QueryFilter protocols. They look like this:

public protocol Queryable {
    associatedtype Filter: QueryFilter
    init(result: Filter.ResultType)
}

public protocol QueryFilter: Equatable {
    associatedtype ResultType: NSFetchRequestResult
    func fetchRequest(_ dataStore: DataStore) -> NSFetchRequest<ResultType>
}

The Queryable protocol is the main type we’ll use for defining the in-memory struct values that we’ll be pulling out of the data store. It has an associated Filter type, which is the type that describes which values to fetch.

It also has an initializer, which takes the actual NSManagedObject instance and uses it to populate its properties. This must unavoidably be part of the interface; I’ve not found a good way to hide this yet.

The QueryFilter protocol defines the associated result type (typically NSManagedObject or one of your app-specific subclasses), as well as a function for turning the filter itself into an NSFetchRequest.

You’ll notice that I pass the DataStore in to the fetch request method. In my experience, the DataStore tends to end up holding cached information that a particular type might find useful when generating the fetch request, and being able to have that information on-hand during predicate creation has proven to be useful.

Adopting this protocol generally is as you’d expect:

public struct Contact: Queryable {
    public typealias Filter = ContactFilter

    public let givenName: String
    public let familyName: String?

    public init(result: MyCoreDataContactObject) {
        self.givenName = result.
        self.familyName = result.
    }
}

public struct ContactFilter: QueryFilter {
    public static let all = ContactFilter()
 
    public func fetchRequest(_ dataStore: DataStore) -> NSFetchRequest<MyCoreDataContactObject> {
        let f = MyCoreDataContactObject.fetchRequest() as NSFetchRequest<MyCoreDataContactObject>
        f.predicate = NSPredicate(value: true)
        f.sortDescriptors = [NSSortDescriptor(key: "givenName", ascending: true)]
        return f
    }
}

With this, we’ve got a simple immutable Contact struct that defines a filter with a single value (.all). The fetch request as a predicate to fetch everything, and it sorts the results in a particular order.

You can imagine having more bells and whistles on a particular QueryFilter type that are unique to the attributes of that type. You will need to implement the NSPredicate generation of course, but you’d have to write those predicates at some layer anyway.

Querying

So, let’s start implementing the basics of @Query:

@propertyWrapper
public struct Query<T: Queryable>: DynamicProperty {
    @Environment(\.dataStore) private var dataStore: DataStore
    @StateObject private var core = Core()
    private let baseFilter: T.Filter

    public var wrappedValue: QueryResults<T> { core.results }

    public init(_ filter: T.Filter) {
        self.baseFilter = filter
    }

    public mutating func update() {
        core.executeQuery(dataStore: dataStore, filter: baseFilter)
    }
}

OK, so far this looks pretty normal. We’ve got a custom property wrapper that will trigger a view update pass in SwiftUI when either the dataStore in the Environment changes, or this “Core” object publishes a “will change” notification. Let’s make a simple first pass at this Core class.

public struct Query<T: Queryable>: DynamicProperty {
    

    private class Core: ObservableObject {
        private(set) var results: QueryResults<T> = QueryResults()

        func executeQuery(dataStore: DataStore, filter: T.Filter) {

            let fetchRequest = filter.fetchRequest(dataStore)
            let context = dataStore.viewContext

            // you MUST leave this as an NSArray
            let results: NSArray = (try? context.fetch(fetchRequest)) ?? NSArray()
            self.results = QueryResults(results: results)
        }
    }

}

This is a really bare-bones implementation. So far, there’s nothing in here that couldn’t be done in the update() method of the property wrapper directly, and we’re always unconditionally executing the fetch request. We’re also not taking advantage of being an ObservableObject and broadcasting changes.

So for round 2, we’ll adjust this. For readability, I’ll leave off the outer layer of indentation.

private class Core: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
    var results = QueryResults<T>()

    var dataStore: DataStore? 
    var filter: T.Filter?

    private var frc: NSFetchedResultsController<T.Filter.ResultType>?

    func fetchIfNecessary() {
        guard let ds = dataStore else { 
            fatalError("Attempting to execute a @Query but the DataStore is not in the environment")
        }
        guard let f = filter else {
            fatalError("Attempting to execute a @Query without a filter")
        }

        var shouldFetch = false

        let request = f.fetchRequest(ds)
        if let controller = frc {
            if controller.fetchRequest.predicate != request.predicate {
                controller.fetchRequest.predicate = request.predicate
                shouldFetch = true
            }
            if controller.fetchRequest.sortDescriptors != request.sortDescriptors {
                controller.fetchRequest.sortDescriptors = request.sortDescriptors
                shouldFetch = true
            }
        } else {
            let controller = NSFetchedResultsController(fetchRequest: request,
                                                        managedObjectContext: ds.viewContext,
                                                        sectionNameKeyPath: nil, cacheName: nil)
            controller.delegate = self
            frc = controller
            shouldFetch = true
        }

        if shouldFetch {
            try? frc?.performFetch()
            let resultsArray = (frc?.fetchedObjects as NSArray?) ?? NSArray()
            results = QueryResults(results: resultsArray)
        }
    }

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        objectWillChange.send()
    }
        
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        let resultsArray = (controller.fetchedObjects as NSArray?) ?? NSArray()
        results = QueryResults(results: resultsArray)
    }
}

This is looking better. The Core class is now an NSObject subclass, so it can be the delegate of an NSFetchedResultsController; this allows us to get notifications from Core Data itself when the underlying data set changes. Or in other words, if we have the results of a @Query shown on screen and the underlying data store mutates the data, the Core object will get a notification, broadcast the objectWillChange signal, and the on-screen view will update to show the new or updated data. We’re checking in the fetchIfNecessary() method to make sure we don’t need to re-fetch data if things haven’t changed.

We’ll need to update the Query wrapper a bit as well:

@propertyWrapper
public struct Query<T: Queryable>: DynamicProperty {
    @Environment(\.dataStore) private var dataStore: DataStore
    @StateObject private var core = Core()
    private let baseFilter: T.Filter

    

    public mutating func update() {
        if core.dataStore == nil { core.dataStore = dataStore }
        if core.filter == nil { core.filter = baseFilter }
        core.fetchIfNecessary()
    }
}

Before getting to the last bit, let’s step back and take a look at what’s going on:

  • The Query wrapper retrieves the data store from the environment, so it knows where to look for data
  • The Query wrapper is created with a filter that describes what should be fetched from the data store
  • Internally, there’s a Core class that uses the data store and filter to create an NSFetchedResultsController; this controller provides the results from the underlying Core Data store, as well as notifies of changes to the data, so our views can properly update. (That update is happening because the Core object calls its objectWillChange.send() method; SwiftUI is seeing this ObservableObject held in a @StateObject and is tracking it for changes.
  • The Query wrapper provides the results back out to the view via this QueryResults type.

The last things to notice before we move on are:

  1. We’re explicitly keeping the results from the controller as an NSArray, not an Array<T.Filter.ResultType>
  2. This array holds NSManagedObject instances, not values of type T
  3. The filter is currently read-only… what if we want to change it dynamically? (Imagine showing a list of results that we want to change the sort order, or start typing to match text, etc)

QueryResults

In order to make our data work nicely with things like List(…) and ForEach(…), it needs to conform to the RandomAccessCollection protocol. So we need to take the array of results we got from Core Data and wrap it up. Fortunately, this only requires a few lines of code:

public struct QueryResults<T: Queryable>: RandomAccessCollection {
    private let results: NSArray
    
    internal init(results: NSArray = NSArray()) {
        self.results = results
    }
    
    public var count: Int { results.count }
    public var startIndex: Int { 0 }
    public var endIndex: Int { count }
    
    public subscript(position: Int) -> T {
        let object = results.object(at: position) as! T.Filter.ResultType
        return T(result: object)
    }
}

This is also where using an NSArray (instead of a Swift Array) is important. When fetching data from Core Data, we don’t always know how many values we’ll be getting back. There could be 5 or 5 million. Core Data solves this problem by using a subclass of NSArray that will dynamically pull in data from the underlying store on demand.

On the other hand, a Swift Array requires having every element in the array all at once, and bridging an NSArray to a Swift Array requires retrieving every single value. So if you execute a query that returns 5 million values and then turn the results into an Array<NSManagedObject>, your app will grind to a halt while it fetches 5 million objects in to memory.

We avoid this by avoiding the NSArray-to-Array conversion. This is fine for our cases, because the Core Data array is smart enough to act as a “random access collection”, so we can easily turn around and ask for the value at the 3 millionth position.

Once we’ve retrieved the value, we run it through the initializer defined on the Queryable protocol, and we end up with the value to show publicly in the UI.

Food for thought: This QueryResults type re-builds the T value every time. How would you add caching so that if it’s built the value once, it doesn’t need to build it again?

Mutating the Filter

It’d be nice to have a way to modify the filter from the UI, so we can control things like sort order or change aspects of the predicate. For example, if you’re showing a list of contacts, it’d be nice to have a search field so you can search for contacts that match some user-provided text. Let’s imagine that the ContactsFilter type has a var searchString = "" property that gets translated into a predicate that searches for that text in the underlying data store.

As before, let’s invent the syntax we want, and then write the code to make it happen:

struct ContactList: View {
    @Query(.all) var contacts: QueryResults<Contact>

    var body: some View {
        TextField("Search", text: $contacts.searchString)
        List(contacts) { contact in
            Text(contact.givenName)
        }
    }
}

That looks pretty neat! If we can pull out a mutable filter (Binding<T.Filter>), we can use the existing mechanism on Binding to scope it down to a single field (@dynamicMemberLookup) and bind the filter directly into a TextField. Let’s make this happen.

The $contacts syntax is some nifty compiler magic to access the projectedValue on the property wrapper (if it has one). We want to implement this on Query and provide a Binding<T.Filter>. Getting the value should return either the current modified filter, or the initial filter. Setting the value should change it on the Core object:

public struct Query<T: Queryable>: DynamicProperty {
    @Environment(\.dataStore) private var dataStore: DataStore
    @StateObject private var core = Core()
    private let baseFilter: T.Filter

    public var wrappedValue: QueryResults<T> { core.results }

    public var projectedValue: Binding<T.Filter> {
        return Binding(get: { core.filter ?? baseFilter },
                       set: {
                        if core.filter != $0 { 
                            core.objectWillChange.send() 
                            core.filter = $0  
                        }
                       })
    }

    
}

There we go! We return a Binding (which is really a wrapper around getter and setter closures) that can get the value by using the core’s filter, and falling back to the baseFilter if the Core doesn’t have one. Setting the value will check to make sure its a different value (so we don’t inadvertently trigger unnecessary UI updates if nothing is changing), then will broadcast an imminent change to SwiftUI, and set the new value into the Core object.

Broadcasting the change means the containing view will be re-built, the Core will notice the changed filter, and the NSFetchedResultsController will be updated and re-executed to provide new results!

Wrapping Up

At the surface, this seems like it’s doing a decent amount of work with little discernible gain. All this, just so I can have a struct value? Well yes… but actually no. As you develop the model layer in your apps, your Filter objects will inevitably become more complex, and having a single place where you build your fetch requests will be invaluable. It makes mutating the underlying managed object model much simpler, because you only have one or two places where you need to change code. You come to appreciate how the compiler stops you from inadvertently mutating values, and since Queryable values typically are structs, you can avoid a decent amount of the queue restriction rules that plague novice Core Data adopters.

In my own usage of this pattern, I’ve found it to be extremely powerful. One neat thing that may not be immediately obvious is that you can have multiple Queryable types that are backed by the same kind of underlying NSManagedObject. So if you have a particularly complex managed object and only need certain parts of it in certain situations, you can construct different Queryable representations for those situations. You can also do things like make your filter conform to Codable to easily make persistable filters.

I’ve also come to really appreciate how this guarantees that the values used to populate my UI are read-only: they’re immutable structs! I couldn’t mutate them if I wanted to! This also allows me to build in-memory values that can be supplemented with additional information. For example, if I update the Queryable initializer to take the DataStore as well as the backing managed object, I have the opportunity to fill out rich value types in ways that Core Data can’t do itself.

Overall, the boilerplate I have to put in to make types be Queryable seems like a good tradeoff for the features, safety, and expressivity I get in return.


So, the big question… Should you use this code?

NO you should not use this code.

There are two main reasons you should hesitate to lift this code into your project:

  1. I wrote all of this in a text editor. None of it has been checked for completeness. I’ve pulled portions of it from my personal implementation, but I’m sure I’ve missed some edge cases that you’ll want to figure out.

  2. Before you take all of this and splat it into your code base, you need to evaluate what problems you’re trying to solve. I came up with this code in an attempt to follow my personal Laws of Core Data, and the design of this code is a reflection of those constraints. Your code may well be working with a different set of a constraints, and so it is imperative that you figure out what is best for your code. Maybe it’s this exactly. Maybe it’s something similar. But maybe it’s completely different. That’s fine.


Related️️ Posts️

Custom Property Wrappers for SwiftUI