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 anNSFetchedResultsController
; 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 theCore
object calls itsobjectWillChange.send()
method; SwiftUI is seeing thisObservableObject
held in a@StateObject
and is tracking it for changes. - The
Query
wrapper provides the results back out to the view via thisQueryResults
type.
The last things to notice before we move on are:
- We’re explicitly keeping the results from the controller as an
NSArray
, not anArray<T.Filter.ResultType>
- This array holds
NSManagedObject
instances, not values of typeT
- 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 theT
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:
-
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.
-
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.