Chapter 9: Core Data and Combine

This text was prepared for publication in March 2020. As a result, a lot of the code and techniques in this book are out of date. It is provided as a historical curio, and does not provide accurate guidance on using SwiftUI today.

Up to this point, you’ve used Swift value types to represent your data models, and have used classes only sparingly. Using strictly immutable copies of value types in this way has many benefits in terms of thread safety, and can make it easier to reason about your data model, but as that model grows—particularly gaining relationships between model items—the burden of maintaining the model’s internal consistency increases. Already you have helper methods in DataCenter to locate related lists or items, special functions to update edited copies of model data in the shared data store, and just in the last chapter you used publishers to monitor the shared store in order to forcibly update your copies of the data should it change. The data model is only going to grow from here on, so it’s worth looking at alternative ways to manage it. One way that already has support for SwiftUI is to use Core Data.

Core Data is Apple’s framework for object graph persistence. Specifically, it takes on two primary roles: it maintains a graph of interrelated objects, ensuring the graph remains consistent and valid over time; and it provides serialization for that object graph in several formats. It’s often thought of as a “database API,” but it’s really not; though one of the storage formats uses SQLite (pronounced ESS-queue-ell-ite, as if it were the name of a mineral), the vast majority of its code deals with the object graph in memory, and database support simply provides an expedient way to load and save parts of the model independently.

In this chapter, you’ll be working almost entirely with the data layer of your application. The implementation of user interface features will take a back seat while you update your application to use a model implemented entirely in terms of Core Data. Much of the fine detail of Core Data itself has been provided for you in the starter project for this chapter, since it would be somewhat outside the bounds of a book on SwiftUI. However, if you’d like to learn more about Core Data, I’d heartily recommend Core Data in Swift by Marcus Zarra.

The starter project for this chapter can by found in the downloadable code archive for this book in the 9-BiggerData/starter folder. There are quite a few changes in here—too many to list the altered files as I’ve done before. Instead, here’s a brief overview of what’s been modified:

  • First and foremost, there is now a Core Data model in Resources/TodoItems.xcdatamodeld.
  • Files relating to the existing struct-based data model have been moved to Model/OldModel, and the type names have been changed to TodoItemStruct and TodoItemListStruct. All the UI code has been updated to refer to these new type names.
  • The Priority type has been lifted out of TodoItemStruct, and TodoItemList.Color is now ListColor.
  • AppDelegate.swift contains the necessary code to load the Core Data model (essentially the same as that provided by an Xcode template).
  • Several helper routines for the Core Data models have been provided, to cut down on the code you’ll need to write.
  • Two new property wrappers have been provided, @DelayedMutable and @DelayedImmutable.
  • A preview-specific class, PreviewDataStore, has been added in Preview Content/PreviewDataStore.swift. This manages a CoreData stack purely for the preview system, recreating its data each time it launches.

I strongly recommend that you use the starter project while working on this chapter; if not, I suggest you use a diff tool such as Kaleidoscope to determine what you’ll need to add to your current project to bring it to parity before continuing.

You’ll take an iterative approach while converting the application, going piece-by-piece from the smallest leaf views towards the top-level containers, getting each component working in the canvas before integrating it into its parent view.

To start with, let’s look at the big picture for a moment, and see how a Core Data model is brought into the world of SwiftUI.

Integrating a Core Data Model

Open SceneDelegate.swift and locate the line where your Home view is created, then make these changes:

// file: "SceneDelegate.swift"
let context = UIApplication.shared.persistentContainer.viewContext
let contentView = Home()
    .environment(\.managedObjectContext, context)

Here you’ve used some convenience accessors in AppDelegate.swift to fetch a property from your AppDelegate named persistentContainer. This is an NSPersistentContainer instance which manages the Core Data “stack” on your behalf—the model description, the on-disk data store, and the in-memory data context. From this you’re fetching the the managed object context designated for the use of the user interface, the viewContext. A managed object context provides the means of interacting with an object graph and its accompanying persistent store; and since Core Data takes a very serious attitude towards thread safety, the use of a single context used exclusively for driving the user interface (and thus synchronized to the main thread) is a particular concern. By separating out the UI’s context like this, everything that happens to the context observed by the interface is guaranteed to take place on the main thread. This has two main benefits:

  • Any UI updates triggered by changes in the data store are guaranteed to happen on the main thread, where the UI expects them to happen.
  • Any work performed by the UI’s object context is likewise guaranteed to happen on the main thread, meaning it will be properly synchronized with the UI framework’s ability to update the interface to match—the model can’t change while it’s being drawn.

This view context is passed into SwiftUI through the environment; specifically, the viewContext is assigned to the environment’s managedObjectContext property. Other views and even property wrappers in SwiftUI will look for it here.

Note that you are no longer passing the DataCenter into the environment: this component is going to be replaced in every view, and by removing it from here you’ll find out very quickly if a view still tries to access it—because your app will quit!

Core Data Model Objects

At the most basic level, there’s not a lot of difference between a Core Data object and a value type, from SwiftUI’s point of view. Both contain properties, and both can be used as a form of state information. For structures, you’ve used the @State property wrapper. For Core Data objects, which are class types, you use @ObservedObject instead. The ObservableObject protocol conformance is already provided by NSManagedObject, the base type in Core Data from which all model objects descend. Each property defined in the model thus has a Publisher available, and any changes made to those properties will cause SwiftUI to re-evaluate the views that use it.

The first place you’ll see this is when updating the to-do item rows. Open TodoItemRow.swift and change its content:

// file: "TodoItemRow.swift"
@ObservedObject var item: TodoItem

var body: some View {
    HStack {
        Button(action: {
            self.item.complete.toggle()
            UIApplication.shared.saveContext()
        }) {
            // «  ...  »
        }
        .padding(.trailing, 6)
        .buttonStyle(HighPriorityButtonStyle())

        VStack(alignment: .leading) {
            Text(item.title ?? "")
                .font(.headline)
                .foregroundColor(.primary)
                .padding(.bottom, 2)
            // «  ...  »
        }
    }
}

On line 1 you now have all the state your row needs—a reference to the model object representing this to-do item. The DataCenter is gone now, as Core Data will handle the collection of model objects and their storage and source of truth for you. Next, the action for the “Complete” button is a lot simpler. On line 7 the lookup-and-swap process is replaced by a single call to save the model data. In fact, the toggle() call above this is all that’s necessary to get the model updated, and this second line exists only to write the new values to persistent storage.

Only one other line needs to change in this view. On line 15 you’re using Swift’s nil-coalescing operator when assigning the item’s title to the Text view. This is because, unlike the data model used earlier in the book, all Core Data model properties (or attributes, to use the correct term) are nullable by default. This applies to any type that would be represented by an object in Objective-C—so strings, dates, data, and so forth would all be nullable. Only the values of primitive types—numbers, booleans—are non-optional in Swift. Now, the model definition may explicitly state that the title attribute is required, not optional. That only applies to the logical models used inside of Core Data, though; it means that the object will be invalid unless it has a title. As far as the runtime is concerned, though, that value can legitimately be nil. When you first create a new model object, all its properties will be either nil or some zero/false value. The object will be invalid until you provide concrete values, but at runtime you’ll need to handle invalid objects with missing values.

This ultimately leaves you with the burden of deciding what to do with nil attribute values in your UI. Here you’re just using an empty string if no title attribute has been set. You’ll see plenty of this pattern through the rest of this book, and you’ll have some fun when it comes time to create non-optional bindings from these optional values.

Previews

That’s it for the item row—almost. The row view is done, nothing else needs to change. However, the preview won’t run at the moment since it’s still written in terms of the old value-type data model. To update it, you’ll use a custom preview-only data store defined in Preview Content/PreviewDataStore.swift, which provides a small API for you to use in your view previews:

// file: "Preview Content/PreviewDataStore.swift"
class PreviewDataStore {
    static let shared: PreviewDataStore
    
    let storeCoordinator: NSPersistentStoreCoordinator
    let objectModel: NSManagedObjectModel
    let viewContext: NSManagedObjectContext
    
    func newBackgroundContext() -> NSManagedObjectContext
    
    var sampleItem: TodoItem
    var sampleList: TodoItemList
}

The store is accessed through a single shared instance, which in turn manages a preview-specific Core Data stack. The persistent data store is deleted and recreated at each startup, so you’ll always be working with the same sample data. Update your preview to use this data store and its sample TodoItem:

// file: "TodoItemRow.swift"
return TodoItemRow(item: PreviewDataStore.shared.sampleItem)
    .padding()
    .previewLayout(.sizeThatFits)
    .environment(\.managedObjectContext, PreviewDataStore.shared.viewContext)

Note that the call to .environmentObject() has been replaced by a .environment() call used to pass in the managed object context from PreviewDataStore.

If you try to run the preview now you’ll see some compilation errors, though, since the TodoList view is still trying to pass a value-type model object into the view. For now, you can stop this error by removing the reference to TodoItemRow within that class: open TodoList.swift and find the call to initialize the TodoItemRow in that view’s body implementation. Replace it with a simple Text view for now:

// file: "TodoList.swift"
NavigationLink(destination: TodoItemDetail(item: item)) {
    Text(item.title)
        .accentColor(self.color(for: item))
}

Now return to TodoItemRow.swift and launch the preview. Your view renders just as it did before, looking no different. That may seem anticlimactic, but considering the changes that have occurred under the hood this is a great outcome!

Binding to Optional Properties

The next leaf component of the application is the TodoItemEditor view, which should be just as straightforward as the row view. Things aren’t that simple, unfortunately: TextField, Picker, DatePicker and friends all operate on bindings to state values. Specifically, bindings to String, Date, etc. Looking at the managed object class—by ⌃⌘-clicking on the title property referenced in TodoItemRow.swift, for instance—and you’ll see again that the properties aren’t quite the same: String? and Date? types won’t work with the simple $-prefix syntax to create viable bindings.

You’ll learn a few different ways of dealing with this type of data in this section. The item editor references plenty of optional properties, including some which are genuinely optional within the model, and can legitimately be nil.

To start with, open TodoItemEditor.swift and replace its properties and init(item:) method:

// file: "TodoItemEditor.swift"
@ObservedObject var item: TodoItem
@Environment(\.managedObjectContext) var objectContext
@Environment(\.presentationMode) var presentationMode

The showItem property remains—you’ll still be using that—but the item property has now been changed from a @Binding to an @ObservedObject, and the DataCenter instance has been replaced by a reference to the managed object context from the environment, using the @Environment property wrapper. Alongside this is a reference to the editor’s presentationMode; you’ll take this opportunity to move the save/cancel functionality into the item editor in the same fashion you used for ListEditor in Chapter 5.

Nil as an Empty Value

Below the state properties, update the notesEditor property as follows:

// file: "TodoItemEditor.swift"
var notesEditor: some View {
    TextView(text: Binding($item.notes, replacingNilWith: ""))
        .padding(.horizontal)
        .navigationBarTitle("Notes: \(item.title ?? localizedNewItemTitle)")
}

There’s not a lot changed in the three lines of the property definition, but what has changed is quite meaningful. Firstly, the Binding initializer on line 2 has changed. If you scroll up to the top of this file, you’ll see the extension you created in Chapter 3; look at the definiteNotes property there and recall its behavior. If the value stored in the model is nil, then an empty string is returned. If an empty string is set as the new value, then nil is stored in the model. The Binding initializer here is defined in Affordances/OptionalBinding.swift, itself part of an open-source library by the author. The extension provides three new initializers for SwiftUI’s Binding property wrapper, all of which wrap some small yet nonzero-sized amount of boilerplate useful when binding to Optional types:

init(_ source: Binding<Value?>, _ defaultValue: Value)
init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool
init(_ source: Binding<Value?>, replacingNilWith nilValue: Value)

The first of these methods provides a version of Binding that enforces a default non-nil value for its target; this is useful for non-optional model properties that may be set to nil when first initialized. The second wraps the question “is my target binding’s value nil?” You’ll see this in action shortly.

The third is the one you’ve used when defining the notesEditor. This performs the same operations as the definiteNotes property noted above; its parameters are an underlying binding to a property of some Optional type and a suitable “empty” value. Here you’ve provided a binding to the notes property—a Binding<String?>—and an “empty” value of "", the empty string. The binding wrapper will silently swap the nil and empty values when accessing the underlying Binding<String?>.

The compiler will likely be complaining about line 4. Here any potential nil value from the item’s title property is replaced by a reasonable (and localized) default (see Non-Optional Optionals below), but that default hasn’t been defined. Scroll up and add the following private value above the definition of the TodoItemEditor type:

// file: "TodoItemEditor.swift"
fileprivate let localizedNewItemTitle = NSLocalizedString(
    "New Item", comment: "default title for new to-do item")

Notice that this uses the NSLocalizedString(_:comment:) method to obtain the localized value as a String rather than a SwiftUI LocalizedStringKey. This is necessary because the title property is an optional String, so any alternative value used with the optional-chaining operator must also be a String. With no public API to obtain a string value from a LocalizedStringKey, falling back to NSLocalizedString() is the only option.

Non-Optional Optionals

You might wonder why you’re going to some effort to handle a nil value for a todo item’s title property. After all, you’re able to ensure that the value is never nil when it’s supplied to an editor or other view. Certainly that can’t be the case for anything loaded from the data store, since title is an explicitly non-optional attribute of the data model. It would seem reasonably save to implicitly unwrap the optional value, using item.title!, since you have control over the data in every case.

Unfortunately, Core Data’s memory model and SwiftUI’s close observance of state property modifications sometimes find themselves at cross purposes. As an example, let’s say you’re creating a new to-do item, so you have the editor open. You change your mind, and click “Cancel.” That button deletes the un-saved item from the object context and dismisses the editor sheet. All clear and good, right?

Alas, no—in between the delete and the dismiss, SwiftUI will detect the change of all the item’s properties, and will re-render the editor view. The view will still have a reference to the TodoItem instance, but this will now be a fault—Core Data parlance for “model object whose values need to be fetched from the store”—so it will attempt to load its properties. You just deleted it from the store, though, so that carefully never-nil property is going to return just that, causing your implicitly unwrapped optional to crash the application.

Binding with Default Values

The next part of the view implementation to changes is the definition of the body property itself. Inside the existing Form view, update the title field and list picker like so:

// file: "TodoItemEditor.swift"
TextField("Title", text: Binding($item.title, localizedNewItemTitle))

Picker("List", selection: Binding(
    $item.list, TodoItemList.defaultList(in: self.objectContext))
) {
    ForEach(TodoItemList.allLists(in: self.objectContext)) { list in
        Text(list.name ?? "<unknown>")
    }
}

For both of these controls, their bindings are defined using the default-value initializer, init(_ source: Binding<Value?>, _ defaultValue: Value). While neither value should ever be nil, the type system doesn’t know that, so you provide a non-optional default to be used if that is the case; the default value will be written to the underlying binding, in fact, making this a shorter form of object.property = value; return $object.property. The title field uses the localizedNewItemTitle value you’ve seen already, while the list picker uses the default list defined in the data model. Both the default list and the collection of all available lists need to be fetched from the view’s object context, so simple functions for these operations have been provided.

The priority picker is unchanged, but the “Has Due Date” Toggle control needs to use another optional-value binding:

// file: "TodoItemEditor.swift"
Toggle("Has Due Date", isOn: Binding(isNotNil: $item.date, defaultValue: Date()))
    .labelsHidden()

This uses the “is not nil” form of binding, which wraps a pair of operations. Firstly, its getter simply returns the result of asking “is $item.date.wrappedValue currently nil?” Its setter, on the other hand, will take a Bool value and parlay that into an appropriate concrete value for $item.date: false will set it to nil, while true will set it to the provided defaultValue—in this case, the current date. This is effectively the same as the hasDueDate property defined at the top of this file, one of whose two uses you’ve just replaced.

To update the date picker itself, you’ll take a similar approach:

// file: "TodoItemEditor.swift"
if self.item.date != nil {
    Toggle("Include Time", isOn: $item.usesTimeOfDay)
    HStack {
        Spacer()
        DatePicker("Due Date", selection: Binding($item.date, Date()),
                   displayedComponents: item.usesTimeOfDay
                       ? [.date, .hourAndMinute]
                       : .date)
            .datePickerStyle(WheelDatePickerStyle())
            .labelsHidden()
        Spacer()
    }
}

Here the use of the hasDueDate property is replaced by a simple nil check, while the binding for the picker’s selection obtains a default value. The picker itself will only be displayed when the date is explicitly non-nil, but again the Swift type system doesn’t know that, so a little work is needed to provide a Binding<Date> rather than a Binding<Date?>. The local showTime state property has been replaced with a binding to a custom property on TodoItem—look in Model/CoreDataHelpers.swift to find its implementation.

Safely Handling Model Updates

Core Data is designed to handle multiple discrete operations on the same data model from a variety of different locations. Data may arrive from the network, it may be modified, normalized, or created automatically during read/write operations, or it could be updated by direct user interaction. SwiftUI’s data-driven interfaces need to interact with all these changes in a safe and synchronized manner, no matter their cause.

Cancellable User Modifications

There are a few different ways to handle cancellable modifications in Core Data. The design of the framework is that every model object is part of a context, and that context loads data from and writes it to a persistent store. Thus any changes you make to a model object won’t actually persist until you ask the context to save(). Until that point, it’s possible to discard any in-memory changes and just refresh the object from the persistent store. If the object is new and has never been saved, you can determine that and just delete it from the store directly. That has some potential issues, though, including but not limited to those described in Non-Optional Optionals.

Amongst other things, the reference-type nature of Core Data model objects means that the same instance is being used everywhere. With SwiftUI monitoring these objects for changes and performing potentially extensive view updates as a result, operating directly on the same shared instance can lead to many side-effects and additional work for the framework. It can also make things a little harder to reason about once your model gets complicated.

For example, consider a later version of the application which stores data both locally and on a cloud server; perhaps it uses Core Data’s built-in iCloud support to do so. While you’re editing some object—you’ve changed the name and due date, perhaps—a change to something else comes through from the network. There are no conflicts, so the change is imported in the background and passed to the main view context, which is then saved to disk. Now you decide to cancel your changes, so the object is reverted back to its saved form—except the entire context it was just saved by someone else, so now the new title and date are stuck in place.

Core Data provides assistance for this through the concept of hierarchical contexts. A given object context may be connected to a persistent data store, or it may be connected to another context. When this context loads, it just asks its parent context for data. When it saves, it just passes its changes along to its parent. With this facility available, it’s become common practice to create a new context to associate with an editor, and constrain all changes to that context alone. Until the editor’s context is saved, the changes are limited to that context alone—to discard them, you just don’t save them. Once they are saved, then you tell the parent context to do the same; generally this task is handled by whatever created the editing context.

This is all building to a single point here: this design provides a nice way to modularise save/cancel functionality into your model editor views. They simply operate in terms of the object context fetched from the SwiftUI environment, and they either save the updated object or not. This means that the “Done” and “Cancel” buttons’ implementations can now be moved into TodoItemEditor directly.

Add the following properties above the editor’s body implementation:

// file: "TodoItemEditor.swift"
var cancelButton: some View {
    Button(action: {
        self.presentationMode.wrappedValue.dismiss()
    }) {
        Text("Cancel")
            .foregroundColor(.accentColor)
    }
}

var doneButton: some View {
    Button(action: {
        try? self.objectContext.save()
        self.presentationMode.wrappedValue.dismiss()
    }) {
        Text("Done")
            .bold()
            .foregroundColor(.accentColor)
    }
}

On lines 3 and 13 you can see the same call to PresentationMode.dismiss() that you used in Chapter 5. When the user chooses to commit their changes, the editor simply instructs the current context to save(), on line 12. If the current context is connected directly to a persistent store, then the data will be written out. If it’s connected to another context, then the changes will be sent to that context. The view that presented the editor will know the details, and will take any necessary additional action, such as saving the underlying context.

Previewing

To make the preview work, you’ll again need to take two steps. Firstly, update the preview provider to use the sample TodoItem and install a managed object context in the environment:

// file: "TodoItemEditor.swift"
NavigationView {
    TodoItemEditor(item: PreviewDataStore.shared.sampleItem)
}
.environment(\.managedObjectContext, PreviewDataStore.shared.viewContext)

Xcode will likely be complaining about lines in two other files, however: the initializer for TodoItemEditor has changed. Open TodoList.swift and locate the editorSheet property definition. At the bottom, replace the content of the NavigationView with an EmptyView. Now open TodoItemDetail.swift and do the same in the editor property there.

Now your preview should launch, and you’ll be able to test-drive the interface to ensure everything still works as it did before.

Before moving on, remove the TodoItemStruct extension toward the top of the file—it isn’t needed any more.

Using Editor Contexts in SwiftUI

In the previous section you learned about the use of ‘editing contexts’ to wall off uncommitted changes from the core data store. Setting this up is actually quite easy in SwiftUI, as you’ll see: the environment provides a form of dependency injection, meaning that your editor views simply operate in terms of the current environment, allowing the parent to adjust that environment to inject values such as a new managed object context.

You’ll put this into action in the item detail view; open TodoItemDetail.swift. Start by updating the state properties as before, deleting the editingItem property while you’re there.

// file: "TodoItemDetail.swift"
@ObservedObject var item: TodoItem
@Environment(\.managedObjectContext) var objectContext

@State var showingEditor = false

While you’re here, use nil-coalescing operators to fix the errors in headerBackground and body, providing default values for the list color and the item title; delete the TodoItemStruct extension at the top of the file; and find the errors identified by Xcode in TodoList.swift where it uses the old TodoItemDetail initializer, replacing them with an EmptyView. With that out of the way, you can proceed with the important task of defining and handling an editing context.

There are four operations involved in this design:

  • Create a new NSManagedObjectContext, setting its parent as the context found in the environment.
  • Obtain a new instance of this view’s TodoItem from that new context.
  • Initialize the editor with the new item, assigning the new context to the editor’s environment.
  • When the editor is dismissed, check if the current context has changes and save it if so.

Creating the Editor Context

The first three tasks take place in the editor property definition:

// file: "TodoItemDetail.swift"
var editor: some View {
    let context = self.objectContext.editingContext()
    guard let editItem = context.realize(self.item) else {
        preconditionFailure("Failed to get edit version of existing item")
    }
    return NavigationView {
        TodoItemEditor(item: editItem)
            .environment(\.managedObjectContext, context)
    }
}

On line 2 you create the editing context, using a utility method found in Model/CoreDataHelpers.swift. If you look at the implementation of editingContext() you’ll see that it simply creates a new NSManagedObjectContext tied to the main thread (it’s going to drive the UI, remember!), and its parent is set to the current context. That relationship will cause the new context’s save() method to just propagate any changes to its parent context.

Similarly, line 2 uses another helper method to obtain a copy of the TodoItem, within the editing context; this uses the identifier of the to-do item to load an instance of it in the new context.

The remainder is almost unchanged, with the exception of line 8. This line puts the new editing context into the environment for the editor view. Now when the editor calls objectContext.save() the changes will be propagated to the item property of the detail view.

Saving Changes

The fourth operation listed above requires a change to the .sheet() view modifier in the body property definition; you’ll add new parameter, providing a block to run when the sheet is dismissed:

// file: "TodoItemDetail.swift"
.sheet(isPresented: $showingEditor, onDismiss: {
    if self.item.hasPersistentChangedValues {
        UIApplication.shared.saveContext()
    }
}, content: { self.editor })

The only new part here is the onDismiss parameter; this looks at the view’s TodoItem and asks if it contains any changes that need to be written to the data store. If it does, then you save the context using a helper method located in AppDelegate.swift. The hasPersistentChangedValues property specifically looks at the actual values that are written to permanent storage, ignoring anything that is calculated dynamically or otherwise transient in nature.

You can now try this out by updating the previews property of TodoItemDetail_Previews at the bottom of this file to use the same parameters and environment values as before:

// file: "TodoItemDetail.swift"
TodoItemDetail(item: PreviewDataStore.shared.sampleItem)
    .environment(\.managedObjectContext, PreviewDataStore.shared.viewContext)

Launch a live preview and bring up the editor. Make some changes and cancel or commit them, and observe how the detail view’s content changes.

Rinse and Repeat

The TodoListEditor view needs very similar changes to bring it back to working order. Open TodoListEditor.swift and replace its state properties:

// file: "TodoListEditor.swift"
@Environment(\.presentationMode) private var presentation
@Environment(\.managedObjectContext) private var objectContext
@ObservedObject var list: TodoItemList

Update the action for the “Done” button to save its object context:

// file: "TodoListEditor.swift"
try? self.objectContext.save()
self.presentation.wrappedValue.dismiss()

And lastly provide some defaults for the optional icon and name attributes:

// file: "TodoItemEditor.swift"
Image(systemName: list.icon ?? "list.bullet")
TextField("List Title", text: Binding($list.name, "New List"))
IconChooser(selectedIcon: Binding($list.icon, "list.bullet"))

Don’t forget to update the preview to try it out.

Displaying Lists

Up to now, you’ve only dealt with one Core Data model object at a time, and that object has always been handed into the view you’re working on. To implement the TodoList, Home, and HomeHeader views, however, you need to learn how to obtain and interact with groups of objects. In TodoList you’ll have the additional burden of managing sort ordering, so let’s start with the more straightforward views first.

Start by opening Home.swift and adding this property near the top of the Home implementation:

// file: "Home.swift"
@FetchRequest<TodoItemList>(
    sortDescriptors: [
        NSSortDescriptor(keyPath: \TodoItemList.manualSortOrder,
                         ascending: true)
    ])
var lists

Here you’ve used a new property wrapper from SwiftUI named @FetchRequest. This provides the primary means by which Core Data objects are brought into a SwiftUI interface, and its wrapped value resolves to a collection of Core Data result types, such as model objects. Each @FetchRequest ultimately wraps a Core Data NSFetchRequest, which itself encapsulates a return type, a sort order, a predicate used to select and filter objects, and more. Internally, @FetchRequest properties monitor the object context installed in the SwiftUI environment to determine when changes occur that affect the objects vended by this property. When that happens, SwiftUI is able to trigger an interface update to present the new data automatically.

Here you’re using an attribute syntax you’ve not seen before. The FetchRequest type implementing the attribute takes initialization parameters itself, so you need to pass those in explicitly as you would for a regular type initializer. The simplest initializer it provides takes an instance of a Core Data NSFetchRequest, but here you only want to specify a type and a sort order, so creating a fetch request manually is a lot of work. Instead you’re using another initializer taking only an NSSortDescriptor used to indicate the desired sort ordering, and the type of object you want to obtain is defined by the generic type parameter, <TodoItemList>.

Even this syntax is rather long, though; to fit within the 80-column space for this book it’s been spread over five lines. For this reason, let’s make use of a convenience initializer from Model/CoreDataHelpers.swift to reduce the amount of typing involved:

// file: "Home.swift"
@FetchRequest<TodoItemList>(ascending: \.manualSortOrder)

That’s better. The model object is specified in the generic type parameter, and with that set Swift only needs the key subpath to build the KeyPath.

Updating List Content

Now it’s time to turn your attention to the view’s body. The list property works just like a regular Swift collection, so it can be passed directly into the ForEach initializer. You’ll also need to provide non-nil values for the list name and icon, as with all Core Data model objects:

// file: "Home.swift"
ForEach(self.lists) { list in
    NavigationLink(destination: TodoList(list: list)) {
        Row(name: list.name ?? "<Unknown>",
            icon: list.icon ?? "list.bullet",
            color: list.color.uiColor)
    }
}

Xcode likely complains about the TodoList() initializer at this point; until you’ve updated that class as well, just replace it with an EmptyView to keep the compiler happy, and move on.

Deleting

The onDelete() and onMove() modifiers are next on the list. Deletion in Core Data is handled by passing the object to be deleted to its object context’s delete() method. Once that’s done, you need to call save() on the context to actually delete the object from persistent storage.

To do this, you’ll need to have access to the object context; add the familiar environment property to the Home implementation to obtain it:

// file: "Home.swift"
@Environment(\.managedObjectContext) var objectContext

Since you’re doing these two operations in a row, it’s a good idea to keep them synchronized with one another; ideally you want to only delete this object, without saving some other change happening at the same moment. NSManagedObjectContext provides two methods to perform this sort of synchronization: perform(_:) and performAndWait(_:). These function similarly to DispatchQueue’s async(_:) and sync(_:) methods—the first will run the supplied block asynchronously and return immediately, while the second will block the calling thread until the block has finished running. The object context used in the UI is tied to the main (UI) thread, so the work will always execute on that thread, nicely synchronized with respect to any user interface updates.

In this case, you don’t need to do anything immediately after the change, so you’ll use perform(). Replace the existing .onDelete() implementation with this new version:

// file: "Home.swift"
.onDelete { offsets in
    self.objectContext.perform {
        for offset in offsets {
            self.objectContext.delete(self.lists[offset])
        }
        try? self.objectContext.save()
    }
}

Note that, since all the Core Data operations are synchronized with the UI thread, you can simply reach into the lists property by index, deleting each item. As the delete doesn’t actually remove anything from memory until you save the context, neither do you need to iterate through the offsets backwards to ensure the indices remain correct. Similarly, while you’re inside the perform() block, SwiftUI can’t update the content of the lists property—it will have to wait until you finish iterating and deleting all the indicated objects.

Threading can be difficult at times, but once you get it right it makes things so much easier!

Reordering

Moving and ordering is a little different, however. By default, all collections in Core Data are considered unordered. You impose a specific ordering on them through the use of an NSSortDescriptor when fetching them from the data store (in fact, the @FetchRequest wrapper outright requires a sort descriptor). This means that you can’t just shuffle items around inside a flat array any more; you’re going to have to take a different approach.

If you’ve inspected the object model—or if you raised an inquisitive eyebrow at TodoItemList.manualSortOrder when defining the fetch request earlier—then you likely see how this is going to be handled. In fact, both the TodoItem and TodoItemList objects have integer attributes named manualSortOrder. This enables you to sort them easily based on that attribute’s value—precisely what the list property is doing in this view. To change the order of the items, then, you need only change the numbers used to order them.

Let’s look at the new .onMove() implementation:

// file: "Home.swift"
.onMove { offsets, index in
    var newOrder = Array(self.lists)
    newOrder.move(fromOffsets: offsets, toOffset: index)
    self.objectContext.perform {
        for (index, list) in newOrder.enumerated() {
            list.manualSortOrder = Int32(index)
        }
        try? self.objectContext.save()
    }
}

Here you’ve created a copy of the list property in an Array, and used SwiftUI’s handy move(fromOffsets:toOffset:) method to rearrange its contents. Next, within another object context perform() block, you enumerate the contents of this array and assign each item a new manualSortOrder equal to its location in the array, thereby providing an ascending order. When the context is saved, the list property will automatically update and the view will be updated to show the lists in their new positions.

Creating New Items

Not much changes when creating new model objects with Core Data. The key point to remember is that all such objects are created within a particular object context. To illustrate, consider the TodoItemList.newList(in:) method in Model/CoreDataHelpers.swift:

// file: "Model/CoreDataHelpers.swift"
static func newList(in context: NSManagedObjectContext) -> TodoItemList {
    let list = TodoItemList(context: context)
    list.name = NSLocalizedString("New List", comment: "Default title for new lists")
    list.icon = "list.bullet"
    list.color = .blue
    list.manualSortOrder = Int32(listCount(in: context))
    return list
}

Note that the TodoItemList initializer requires a reference to the object context in which it will live. The remainder of the implementation, though is straightforward: some default values are assigned for all the non-optional (in the model-definition sense, not the language-property sense) properties.

Aside from that, not much has changed from the struct-based implementation; once the new object is created, it can be displayed in an editor. Here again you would create a child object context to place into the editor’s environment, creating the new list within that context. You would also add an onDismiss callback to the .sheet() view modifier to save the local object context when the sheet is dismissed. The result should look something like this:

// file: "Home.swift"
var body: some View {
    NavigationView {
        // «  ...  »
    }
    .sheet(isPresented: $showingEditor,
           onDismiss: { try? self.objectContext.save() },
           content: { self.newListEditor })
}

Update the preview provider to place PreviewDataStore.shared.viewContext into the environment, and check out the results on the canvas.

Model Validation

The model definition will enforce validity when objects are saved to the persistent store, so a list with no name, for example, will only raise an error during the call to save(). The examples you’ve seen so far have been generally ignoring these errors for the sake of expediency, using try? and ignoring the result. In practice, this is far from ideal. Imagine the situation: one editor makes an invalid change to one object, so the save fails—but the invalid object remains in the context. From that point on, every call to save() that context will fail, because it will include that invalid object.

It is possible to handle these issues more gracefully than is shown in the example code. For instance, when the user clicks “Done” in an editor, the object can be explicitly validated by calling validateForUpdate(). If the object is invalid, an error will be raised with lots of helpful information attached. That can then be used to present a dialog to the user instead of merely closing the sheet.

Aside from looking at direct validation, it’s also good to catch and inspect all errors whenever they occur. Core Data’s errors are particularly full of useful information, and will almost always lead you directly to a solution. While expediency (and page count!) necessitates a more cavalier approach in this book, you can find an example of some simple error handling in AppDelegate.swift, in saveContext().

Dynamically Sorting Collections

So far you’ve worked with individual model objects, both displaying and editing them. All these objects have either been provided as parameters or fetched from the data store using a static request and sort order. SwiftUI provides enough tooling for those operations, but stepping beyond them requires somewhat more vigilance. For instance, how does one change the sort order of a collection loaded through a @FetchRequest property? Well… one doesn’t; it isn’t possible to reach inside the property wrapper to update its request’s parameters. Nor is it possible to keep the underlying NSFetchRequest around to mutate it on demand, because @FetchRequest copies its input rather than retaining it.

To solve this issue, you’ll make use of a new @MutableFetchRequest wrapper, located in Affordances/MutableFetchRequest.swift, also a part of the author’s open-source toolset. This will provide you the ability to swap out the underlying NSFetchRequest dynamically, in turn driving many of the features of the TodoList view.

First, though, some book-keeping is required. The SortOption type needs to be updated to think in terms of Core Data. Since a fetch request uses an array of NSSortDescriptors to define the ordering of its result, you will need to provide that array, and the most prudent way to do that is to have the SortOption vend it directly. Open TodoList.swift and add the following computed property to SortOption:

// file: "TodoList.swift"
var sortDescriptors: [NSSortDescriptor] {
    switch self {
    case .title:
        return [NSSortDescriptor(keyPath: \TodoItem.sortingTitle,
                                 ascending: true)]
    case .priority:
        return [NSSortDescriptor(keyPath: \TodoItem.rawPriority,
                                 ascending: false)]
    case .dueDate:
        return [NSSortDescriptor(keyPath: \TodoItem.date,
                                 ascending: true)]
    case .manual:
        return [NSSortDescriptor(keyPath: \TodoItem.manualSortOrder,
                                 ascending: true)]
    }
}

Each sort option now provides a set of sort descriptors, specifying what model value should be compared to create the sort order, and whether higher or lower values should come first. Most use an ascending order—A to Z, past to future—though the priority ordering is the reverse, since the highest priority items should be most prominent.

Within the priority and dueDate cases, however, it’s possible there will be duplicate values. More than one item of normal priority, for example, or two items due on the same day. In those cases, there is no clear guarantee of the relative ordering of these items; it all depends on what the underlying storage format happens to use. It would be better to provide a secondary option in those cases, so update the results for .priority and .dueDate to include the sort descriptor from .manual as a second element in the array.

Now look at the ListData type at the top of TodoList; this is still using the old data types, so you’ll need to update it. At the same time, add a reference to the managed object context from the environment and declare the @MutableFetchRequest property that will provide your to-do items:

// file: "TodoList.swift"
private enum ListData {
    case list(TodoItemList)
    case items(LocalizedStringKey, NSFetchRequest<TodoItem>)
    case group(TodoItemGroup)
}

@Environment(\.managedObjectContext) var objectContext

@State private var listData: ListData
@MutableFetchRequest<TodoItem> var items: MutableFetchedResults<TodoItem>

Note that the items property on line 10 isn’t initialized. Unlike the @FetchRequest you used in Home.swift, this hasn’t been given a fetch request or sort descriptors, and thus is only a declaration, not a definition. To give it a value, you’ll need to update your initializers:

// file: "TodoList.swift"
init(list: TodoItemList) {
    self._listData = State(wrappedValue: .list(list))
    let request = list.requestForAllItems
    request.sortDescriptors = SortOption.manual.sortDescriptors
    self._items = MutableFetchRequest(fetchRequest: request)
}

init(title: LocalizedStringKey, fetchRequest: NSFetchRequest<TodoItem>) {
    let request = fetchRequest.copy() as! NSFetchRequest<TodoItem>
    request.sortDescriptors = SortOption.manual.sortDescriptors
    self._listData = State(wrappedValue: .items(title, request))
    self._items = MutableFetchRequest(fetchRequest: request)
}

init(group: TodoItemGroup) {
    self._listData = State(wrappedValue: .group(group))
    let request = group.fetchRequest
    request.sortDescriptors = SortOption.manual.sortDescriptors
    self._items = MutableFetchRequest(fetchRequest: request)
}

Each initializer still corresponds to a single form of ListData, but now these values are also used to generate the fetch request required to initialize the items property (or more properly, the _items property). In every case, the initial sort descriptors are those for SortOption.manual.

The first two initializers are quite direct: the first uses a helper property to obtain a request for all the items in the list; the second copies the input fetch request. The third is a little more involved, obtaining its fetch request from the attached TodoItemGroup. Item groups all contain quite complex sets of items, though: anything with a date; anything with a date in the past; anything due today that hasn’t been completed. These are all more complex than the other requests you’ve used so far, so let’s take a look at how they work.

Open Affordances/ItemGroups.swift and scroll to the bottom to find the fetchRequest property. This creates an NSFetchRequest for the TodoItem type, specifies that items should be loaded from storage in batches of up to 25 at a time, and then assigns a predicate. Predicates are instances of NSPredicate, which is a base class defining an interface whereby a single object can be inspected for conformance to a set of rules. These might be name == "Henry", identifier IN (22, 23, 29), and so on—to an SQL veteran, they should look vaguely familiar. These are both instances of comparison predicates, which compare some property against some value. There are also compound predicates, which can require all, some, or none of a set of smaller predicates to evaluate as true. By combining these types, just about any set of criteria can be expressed, including those implied by the TodoItemGroup values cited above.

Just above fetchRequest is the implementation of the fetchPredicate property, which has one case filled out, for .today. This is defined using the predicates API rather than format strings because, well, that’s generally a good habit to get into (parsing the format argument is relatively expensive and should be done only when strictly necessary). The .scheduled, .overdue, and .all cases currently return nil, though; let’s fix that.

Using Predicates

For the .scheduled case, you need to locate any items that have a date set. Alternatively, by turning that clause around, you arrive at “any items whose date is not nil.” That can be easily expressed with a format string:

NSPredicate(format: "date != nil")

The .overdue case can be handled with a similarly straightforward clause, this time with a parameter:

NSPredicate(format: "date < %@", Date() as NSDate)

Note that the parameter is explicitly typed as an NSDate instance; trying to pass a Swift Date will raise an error in Xcode.

The .all case is the simplest, it turns out: it doesn’t need to change at all, since you genuinely want all the TodoItem instances from the data store.

For extra credit, can you implement these in a similar manner to the .today case, using the API rather than a string? The final project code for this chapter includes an example of the correct way to do this.

The upshot of having your filtering and sorting take place inside the fetch request is that you no longer need to perform any sorting yourself. Return to TodoList.swift and remove the sortedItems variable—you’ll replace it shortly—and replace all uses of sortedItems with items, like so:

ForEach(items) { item in
    NavigationLink(destination: TodoItemDetail(item: item)) {
        TodoItemRow(item: item)
            .accentColor(self.color(for: item))
    }
}

While you’re here, you’ll note that you can restore the TodoItemRow initializer, since both views now use Core Data model objects.

Dynamically Updating Fetch Requests

To change the sort descriptors in your @MutableFetchRequest property, you’ll need to proactively update the new sort descriptors and replace them within the property wrapper. Scroll down to the now-empty extension labeled “Sorting” and add a new function:

// file: "TodoList.swift"
private func updateSortDescriptors(_ sortOption: SortOption) {
    let request = self._items.fetchRequest
    if case .group(.all) = self.listData, case .manual = sortOption {
        let listOrder = NSSortDescriptor(key: "list.manualSortOrder",
                                         ascending: true)
        request.sortDescriptors = [listOrder] + sortOption.sortDescriptors
    }
    else {
        request.sortDescriptors = sortOption.sortDescriptors
    }
    self._items.fetchRequest = request
}

This method performs three tasks:

  • On line 2 it obtains the current NSFetchRequest from the _items property (recall that placing an underscore before the property name will let you access the wrapper rather than the content).
  • It assigns the new sort descriptors based on the input SortOption. Note that there is an additional sort descriptor added on lines 4–5 if the TodoList is showing all items with a manual sort order: it first orders items by their list’s manual ordering, to group them together visually.
  • Lastly, the updated NSFetchRequest is placed back into the @MutableFetchRequest property wrapper on line 11, which will cause the list content to change and SwiftUI to re-render the view.

Now that you have this method in place, you need to call it. Locate the .alert() modifier in the body property. Delete the assignment to self.sortBy and replace it with a call to the function you just created.

The remaining steps necessary to adapt the TodoList to use Core Data should all be familiar by this point. First, add an onDismiss parameter to the .sheet() modifier to save the object context when the sheet is dismissed:

// file: "TodoList.swift"
List{
    // « ... »
}
« ... »
.sheet(
    item: $presentedSheet,
    onDismiss: { try? self.objectContext.save() },
    content: presentEditor(of:))

Now remove lines referencing any removed properties, such as editingItem, and replace any remaining uses of the old struct-based model types with their new Core Data versions. Add handling for optional properties within your model objects, either using implicit unwrapping or nil-coalescing operators.

Next update the removeTodoItems() and moveTodoItems() implementations using the approaches from the Deleting and Reordering sections respectively; the other properties and methods in the “Model Manipulation” extension can be removed:

// file: "TodoList.swift"
private func removeTodoItems(atOffsets offsets: IndexSet) {
    for index in offsets {
        self.objectContext.delete(self.items[index])
    }
    UIApplication.shared.saveContext()
}

private func moveTodoItems(fromOffsets offsets: IndexSet, to newIndex: Int) {
    var newOrder = Array(items)
    newOrder.move(fromOffsets: offsets, toOffset: newIndex)
    for (index, item) in newOrder.enumerated() {
        item.manualSortOrder = Int32(index)
    }
    UIApplication.shared.saveContext()
}

Now change the implementation of presentEditor(of:) to use an editing context, similar to the implementation used in TodoItemDetail in Creating the Editor Context:

// file: "TodoList.swift"
case .listEditor:
    guard let list = self.list else {
        preconditionFailure("List editor presented with no list!")
    }
    let context = self.objectContext.editingContext()
    guard let editList = context.realize(list) else {
        preconditionFailure("List editor: can't create editing list!")
    }
    return AnyView(TodoListEditor(list: editList)
        .environment(\.managedObjectContext, context))
}

Lastly, update the content of the editorSheet computed property to use an editing object context. The editor sheet update will use methods from Model/CoreDataHelpers.swift to create a new TodoItem and to locate a suitable list. If this view is displaying a single list, then that would be used; otherwise, the default list is fetched using TodoItemList.defaultList(in:):

// file: "TodoList.swift"
private var editorSheet: some View {
    let editContext = objectContext.editingContext()
    let editList: TodoItemList
    if let list = self.list {
        editList = editContext.realize(list)!
    }
    else {
        editList = TodoItemList.defaultList(in: editContext)
    }
    let editingItem = TodoItem.newTodoItem(in: editList)
    
    return NavigationView {
        TodoItemEditor(item: editingItem)
            .environment(\.managedObjectContext, editContext)
    }
}

Dynamically Monitoring Metadata

The remaining view has a requirement that’s difficult to model with the tools you’ve used so far. Each HeaderItem view needs to display a counter showing the number of items that match; for example, the number of items due today, or the number overdue. This could be implemented using an @FetchRequest and simply accessing the resulting collection’s count property, but that involves a lot more work than is strictly necessary. If you’ve used SQL, for instance, you know that it’s easier to request how many items there are than to fetch something from each one. The same is true in Core Data, and NSManagedObjectContext provides a helper routine for just that purpose: count(for:) takes an NSFetchRequest and returns an Int describing how many objects match, without actually loading any data.

To implement this behavior in a suitably lightweight manner, let’s use the Combine framework to create an operation on top of a notification issued by Core Data. Specifically, when an object context is saved, it posts a notification named NSManagedObjectContextDidSave; you can ask the system NotificationCenter to provide a Publisher that vends these notifications when they occur, and then attach further operations to ultimately vend a counter value.

To start, you’ll need to add some new state. Open HomeHeader.swift and add import statements for Combine and CoreData to the top of the file. Next add the following properties to the HeaderItem type:

// file: "HomeHeader.swift"
@Environment(\.managedObjectContext) var objectContext
@State private var countCancellable: AnyCancellable? = nil

The second property here is a Combine type that acts as a cancellation and continuation token for a publisher. Its purpose is twofold: firstly, it provides a way to explicitly shut down a publisher by providing a cancel() method. Secondly, its existence will keep the publisher alive until you either cancel it or discard the Cancellable instance (which will cancel on your behalf).

Watching Notifications

To start and stop monitoring the matching item count, you’ll need to create a publisher and cancel it, respectively. Add these methods to HeaderItem to implement this:

// file: "HomeHeader.swift"
private func startWatchingCount() {
    guard countCancellable == nil else { return }
    
    let request = group.fetchRequest
    countCancellable = NotificationCenter.default
        .publisher(
            for: .NSManagedObjectContextDidSave,
            object: objectContext)
        .receive(on: RunLoop.main)
        .compactMap { $0.object as? NSManagedObjectContext }
        .tryMap { try $0.count(for: request) }
        .replaceError(with: 0)
        .removeDuplicates()
        .assign(to: \.itemCount, on: self)
    
    if let count = try? objectContext.count(for: request) {
        itemCount = count
    }
}

private func stopWatchingCount() {
    countCancellable = nil
}

Here you’ve obtained an initial publisher using NotificationCenter’s publisher(for:object:) method. On line 9 you ensure that any events from the publisher are delivered on the main runloop, to properly synchronize with the user interface. Every following operation will take place on the main thread.

The following operator, .compactMap(), will attempt to fetch the managed object context from the notification. If it fails (i.e. returns nil), then nothing more will happen—the following operations are guaranteed to receive a non-nil NSManagedObjectContext instance. This is then used by the .tryMap() operator to call count(for:).

The publisher at this point might vend either an Int or an Error. To handle—well, guard against—the latter, line 12 uses the replaceError(with:) operator to catch any errors and publish an Int value instead; in this case 0. A removeDuplicates() operator then ensures that events will only be published if they actually vend a different value than their previous output.

Lastly, the assign(to:on:) operator will take that value and assign it to the itemCount state property, thus triggering SwiftUI to perform a view update. With this in place, any changes to the content of the data store will automatically trigger a view update to display the new value. There’s one final important step, however, which is (for your humble author at least) quite easy to overlook: setting the initial value, which happens on line 17. Without this, the counter would read 0 until something somewhere was modified, which certainly led to some confusion while writing this chapter…

Choosing the Right Moment

Ordinarily, SwiftUI will respond to any state variable update at any location in the view hierarchy. For instance, if you drilled down from the home view into a list, then created a new item, then upon saving that item the counter in one or more ItemHeader instances would update. SwiftUI would then redraw them, despite their being offscreen.

In this particular case the property is fairly innocuous—the state property is only used to set the content of a Text view—but other property updates might have unexpected knock-on effects. What would happen if these items were interactive only when their count was non-zero, perhaps by removing their NavigationLink, or by setting a different destination view? If the user tapped on “Overdue” and either deleted everything there or marked them complete, would the current view disappear? Be replaced by a different view? Stay on screen until the user exited? If the latter, would interactions with this view’s contents still work?

Where possible, it’s useful to think about when it’s appropriate to update your state variables and thus trigger a view redraw. Having some parent view change further up the navigation stack in some unexpected manner might lead to some strange behavior or even bugs, so it’s useful to know how to disable these effects. To illustrate this, let’s start and stop monitoring the item count changes when the view appears and disappears:

// file: "HomeHeader.swift"
var body: some View {
    VStack(alignment: .leading) {
        // « ... »
    }
    .padding()
    .background(
        // « ... »
    )
    .onAppear(perform: startWatchingCount)
    .onDisappear(perform: stopWatchingCount)
}

With these two lines, you’ve arranged for the publisher to only be active while the item header is actually visible. As soon as it goes offscreen—for instance when the user selects either a header item or a list—then the publisher will be cancelled. When the view returns, it will be recreated.

Finalization

You’re almost done with your conversion now; only a little of the HomeHeader itself remains to be updated. You’ll need to change the content of the inner ForEach class to properly define the navigation links. It turns out (whether by design or bug) that SwiftUI doesn’t automatically pass on the entire environment to the destination of a NavigationLink. This works for items within the body of a List, but for this header you’ll have to do it yourself. This becomes more manageable with a little refactoring.

First, add the following to the HomeHeader definition:

// file: "HomeHeader.swift"
@Environment(\.managedObjectContext) var objectContext

private func linkView(for group: TodoItemGroup) -> some View {
    let destination = TodoList(group: group)
        .environment(\.managedObjectContext, objectContext)
    return NavigationLink(destination: destination) {
        HeaderItem(group: group)
    }
}

That factors out all the necessary work to pass on the object context to the destination of the NavigationLink, leaving only one small change to the view’s body:

// file: "HomeHeader.swift"
var body: some View {
    VStack {
        ForEach(Self.layout, id: \.self) { row in
            HStack(spacing: 12) {
                ForEach(row, id: \.self, content: self.linkView(for:))
            }
        }
    }
}

Now take a quick look through the application and ensure that all the navigation links are correctly updated to point to the right views once more, then launch the app and try it out. The experience should match exactly what you had at the end of the previous chapter—conversion successful!

What You Learned

There are many nuances required when dealing with Core Data in SwiftUI. Several times in this chapter you’ve reached out to new and updated types just to make it all a little more manageable. Now, though, you have experience, and the scars to prove it.

  • You can attach a new Core Data model to an existing application.
  • You know how to pass Core Data model objects through a SwiftUI view hierarchy.
  • Several complex tasks have become simpler at the callsite through the use of facilities like NSSortDescriptor and NSPredicate, saving you the effort of implementing sorting and matching algorithms in multiple places.
  • Similarly, implementing NSItemProvider support is now significantly easier.
  • Creating bindings to optional types is actually straightforward, now that you know how it’s done.
  • The @FetchRequest property wrapper can significantly help to manage dynamic collection views.
  • When you need a little more dynamism than @FetchRequest provides, you have the tools to go deeper, whether via notifications and publisher data flows, or through more complex tools such as @MutableFetchRequest.

It’s been a long journey, but you’ve now worked with everything SwiftUI has to offer on iOS and iPadOS. Still more awaits in macOS, watchOS, and tvOS, but the broad strokes are the same, and it should all look quite familiar to you at this point.

You’re ready to take the next steps on your own, and there’s going to be plenty more for you to work with soon enough. Remember that this is only the first public release of SwiftUI, and it will grow to encompass more possibilities as time goes by. It’s all just starting, and now you get to say: you were there.


© 2009-2019. All rights reserved.

Powered by Hydejack v9.2.1