CoreData With SwiftUI
CoreData With SwiftUI
for Masterminds
Core Data
with SwiftUI
Learn how to store data in a database with Core Data and SwiftUI
J.D Gauchat
www.jdgauchat.com
Quick Guides for Masterminds
Copyright © 2022 John D Gauchat
All Rights Reserved
Apple™, iPhone™, iPad™, Mac™, among others mentioned in this work, are
trademarks of Apple Inc.
CORE DATA
DATA MODEL
CORE DATA STACK
MANAGED OBJECTS
FETCH REQUEST
ASYNCHRONOUS ACCESS
CORE DATA APPLICATION
PREVIEWS
SORT DESCRIPTORS
PREDICATES
DELETE OBJECTS
MODIFY OBJECTS
CUSTOM FETCH REQUESTS
SECTIONS
TO-MANY RELATIONSHIPS
CORE DATA
Core Data is the system used to store structured data on the device. Core
Data is an Object Graph manager that defines and manages its own objects
and connections and stores them in a database for easy access. We
determine the composition of the objects and the relationships depending
on the information we want to store and the system takes care of encoding
and decoding the objects, preserving consistency and maximizing efficiency
to quickly write and read data.
Data Model
The structure of the Core Data’s Object Graph is defined with a data model.
A Core Data model is a definition of the type of objects the graph is going
to contain (called Entities) and their connections (called Relationships).
A model can be created from code, but Xcode offers a practical editor to
define the structure of the graph. The model is stored in a file and then the
file is compiled and included in the Core Data system created for our
application. Xcode offers a template to create this file.
The file may be created with any name we want but it must have the
extension xcdatamodel. Once created, it is included in our project along
with the rest of the files. Clicking on it reveals the Xcode editor in the
Editor Area.

The model contains three main components: Entities, Attributes, and
Relationships. Entities are the objects, Attributes are the objects’
properties, and Relationships are the connections between objects. The
first step is to add Entities to the model. Entities are created from the Add
Entity button at the bottom of the editor (Figure 2, number 1). When we
press this button, Xcode creates an entity with the generic name "Entity".
Every attribute must be associated with a data type for the objects to know
what kind of values they can manage (Figure 4, number 2). Clicking on the
attribute’s type, we can open a menu to select the right data type. The
most frequently used are Integer 16, Integer 32, Integer 64, Double, Float,
String, Boolean, Date, and Binary Data. The Integer 16, 32, or 64 options
are for Int16, Int32, and Int64 values, Double and Float are for Double and Float
values, String is for String values, Boolean is for Bool values, Date is for Date
values, and Binary Data is for Data values.
An entity may contain as many attributes as our objects need. For example,
we may add a few more attributes to complement the information
required for books.

In this example, we have added an attribute called year to store the year in
which the book was published, and two attributes of type Binary Data to
store images (the book's cover and thumbnail). The data types used by
these attributes are analog to Swift data types. The title attribute takes a
String value, the year attribute stores a value of type Int32, and the images
are going to be stored as Data structures.
Most values don't require much consideration, but images are made of big
chunks of data. Storing large amounts of data in a Persistent Store can
affect the system's performance and slow down essential processes like
searching for values or migrating the model. One alternative is to store the
images in separate files, but it can get cumbersome to coordinate
hundreds of files with the data in the database. Fortunately, Core Data can
perform the process for us. All we need to do is to store the image as
Binary Data and select the option Allows External Storage, available in the
Data Model inspector panel inside the Utilities Area, as shown below. After
the option is selected, the images assigned to that attribute are stored in
separate files managed by the system.
We could have also included another attribute for the author’s name, but
here is when we need to think about the structure of the Object Graph and
how the information will be stored. If we include a String type attribute for
the author's name inside the Books entity, every time the user inserts a
new book it will have to type the name of the author. This is error prone,
time consuming, and when several books of the same author are available,
it is impossible to make sure that all share the same exact name (for
example, one book could have the author’s middle name and others just
the first one). Without the certainty of having the exact same name, we
can never incorporate features in our app such as ordering the books by
author or getting the list of books written by a particular author. Things get
worse when, along with the name, we also decide to store other
information about the author, like his or her date of birth or their
nationality. A proper organization of this information demands separate
objects and therefore we must create new entities to represent them.
Additional entities are added to the model in the same way as we did with
the first one. Figure 7, below, shows our model with a new entity called
Authors containing an attribute called name.
A relationship only needs two values: its name (the name of the property)
and the destination (the type of objects it is referencing), but it requires
some parameters to be set. We must tell the model if the relationship is
going to be optional, define its type (To-One or To-Many), and determine
what should happen to the destination object if the source object is
deleted (the Delete Rule). All these options are available in the Data Model
Inspector panel when the relationship is selected, as shown below.
By default, the relationship is set as Optional, which means that the source
may be connected to a destination object or not (a book can have an
author or not), the Type of the relationship is set to To-One (a book can
only have one author), and the Delete Rule is set to Nullify. The following
are all the values available for this rule.
To find the right rule for a relationship, we must think in terms of the
information we are dealing with. Is it right to delete the author if one of its
books is deleted? In our case, the answer is simple. An author can have
more than one book, so we cannot delete the author when we delete a
book because there could be other books that are connected to that same
author. Therefore, the Nullify rule set by default is the right one for this
relationship. But this could change when we create the opposite
relationship, connecting the Authors entity to the Books entity. We need
this second relationship to search for books that belong to an author.
Figure 10 shows a relationship called books that we have created for the
Authors entity.
IMPORTANT: The Delete Rules are a way to ensure that the objects
remaining in the Object Graph are those that our application and the
user need. But we can always set the rule to Nullify and take care of
deleting all the objects ourselves.
There is a third value for the relationship called Inverse. Once we set the
relationships on both sides, it is highly recommended to set this value. It
just tells the model what the name of the opposite relationship is. Core
Data needs this to ensure the consistency of the Object Graph. Figure 11
shows the final setup for both relationships.
The creation of the model is just the first step in the definition of the Core
Data system. Once we have all the entities along with their attributes and
relationships set up, we must initialize Core Data. Core Data is created from
a group of objects that are in charge of all the processes required to
manage the data, from the organization of the Object Graph to the storage
of the graph in a database. There is an object that manages the model, an
object that stores the data on file, and an object that intermediates
between this Persistent Store and our own code. The scheme is called
Stack. Figure 13 illustrates a common Core Data stack.
The code in our application interacts with the Context to manage the
objects it needs to access and their values, the Context asks the Persistent
Store to read or add new objects to the graph, and the Persistent Store
processes the Object Graph and saves it in a file.
The Core Data framework offers classes to create objects that represent
every part of the stack. The NSManagedObjectModel class manages the model,
the NSPersistentStore class manages a Persistent Store, the
NSPersistentStoreCoordinator class is used to manage all the Persistent Stores
available (a Core Data stack can have multiple Persistent Stores), and the
NSManagedObjectContext creates and manages the context that intermediates
between our app and the store. Although we can instantiate these objects
and create the stack ourselves, the framework offers the NSPersistentContainer
class that takes care of everything for us. The class includes the following
initializer and properties to access each object of the stack.
loadPersistentStores(completionHandler: Closure)—This
method loads the Persistent Stores and executes a closure when the
process is over. The closure receives two arguments, an
NSPersistentStoreDescription object with the configuration of the stack, and
an optional Error value to report errors.
All the communication between our app and the data in the Persistent
Store is done through the context. The context is created by the container
from the NSManagedObjectContext class. This class includes properties and
methods to manage the context and the objects in the Persistent Store.
The following are the most frequently used.
When working with Core Data, the Core Data's Persistent Store becomes
our app's data model. Therefore, we can define a specific class to initialize
the Core Data stack, or just do it from our model, as in the following
example.
init() {
container = NSPersistentContainer(name: "books")
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

The ApplicationData class in this example defines a property called container to
store a reference to the Persistent Store. When the object is initialized, we
create an instance of the NSPersistentContainer class with the name of the
Core Data model (in our example, we called it "books") and configure it to
merge the changes between the context and the Persistent Store (this is
the configuration recommended by Apple). The object creates the stack
but does not load the Persistent Stores, we have to do it ourselves with the
loadPersistentStores() method. After completion, this method executes a
closure with two values: a reference to the Persistent Store just created,
and an Error value to report errors. Errors are infrequent, but if an error
occurs, we should warn the user. For instance, we can modify a state
property to open an Alert View to report the situation. In this example, we
just call the fatalError() function to stop the execution of the app.
Once we have the container, we must get the context from it and share it
with the views, so they can add, fetch, or remove objects from the
Persistent Store. The environment offers the managedObjectContext property
for this purpose. The following are the modifications we must introduced
to the App structure to inject a reference to the context into the
environment.
@main
struct TestApp: App {
@StateObject var appData = ApplicationData()
Core Data does not store our custom objects; it defines a class called
NSManagedObject for this purpose. Every time we want to store information
in the database, we must create an NSManagedObject object, associate that
object to an Entity, and store the data the entity allows. For example, if we
create an object associated to the Books entity, we are only allowed to
store five values that corresponds to the Entity's attributes and relationship
(title, year, cover, thumbnail, and author). The class includes the following
initializer and methods to create and manage the objects.
NSManagedObject(context: NSManagedObjectContext)—This
initializer creates a new instance of the NSManagedObject class, or a
subclass of it, and adds it to the context specified by the context
argument.
fetchRequest()—This type method generates a fetch request for an
entity. A fetch request is a request we use to fetch objects of a
particular entity from the Persistent Store.
entity()—This type method returns a reference to the entity from
which the managed object was created. It is an object of type
NSEntityDescription with a description of the entity.
To ask Xcode to create the subclasses for us, we must select the entities
one by one, click on the Class Definition value for the Codegen option
(Figure 14, number 2), and make sure that the name of the subclass is
specified in the Name field (Figure 14, number 1). Once the options are
set, the classes are automatically created. For example, when we set these
options for the entities in our model, Xcode creates a subclass of
NSManagedObject called Books with the properties title, year, cover, thumbnail, and
author, and a subclass called Authors with the properties name and books. From
now on, all we need to do to store a book in the Persistent Store is to
create an instance of the Books class using the NSManagedObject initializer.
Do It Yourself: Click on the Core Data model file to open the editor.
Select the Books and the Authors entity, open the Data Model
Inspector panel, and make sure that the value of the Codegen option
is set to Class Definition and the name of the class is assigned to the
Name field, as shown in Figure 14.
Every time we need to get data from the Persistent Store, we must create
an NSFetchRequest object to determine what type of objects we want. To
simplify our work, SwiftUI includes the @FetchRequest property wrapper.
With this property wrapper, we can create a request or apply our own
NSFetchRequest object. The property wrapper takes care of performing the
request and updating the views with the values returned. It is created from
a structure of type FetchRequest. The following are some of the structure's
initializers.
A fetch request loads all the objects available in the Persistent Store. This is
not a problem when the number of objects is not significant. But a
Persistent Store can manage thousands of objects, which can consume
resources that the app and the system need to run. Therefore, instead of
the fetch request, the @FetchRequest property wrapper produces a value of
type FetchedResults. This is a structure that takes care of loading into the
context only the objects that are required by the view at any given
moment.
Asynchronous Access
It is time to see how all these tools work together to store and retrieve
data from a Persistent Store in a real application. The purpose of this
application is to show the list of objects stored in the Persistent Store and
create new ones.
The initial view lists all the books available in the Persistent Store and
includes a button to open a second view to create new objects with the
values provided by the user.
All the interaction between our code and the Persistent Store is done
through the context. When we want to access the objects already stored,
add new ones, remove them, or modify any of their values, we have to do
it in the context and then move those changes from the context to the
Persistent Store. The @FetchRequest property wrapper automatically gets a
reference of the context from the environment (this is the reference we
injected into the context in the App structure of Listing 2), but when
working directly with Core Data, we must get the reference from the
environment with the @Environment property wrapper and the
managedObjectContext key. In this example, we called this property dbContext.
The view includes two TextField views to let the user insert the title of the
book and the year of publication, and a button in the navigation bar to save
the values. When the button is pressed, the code checks if the values are
valid and then calls an asynchronous method to create and store a new
object in the Persistent Store. We moved the code to an asynchronous
method so that we can implement the perform() method provided by the
context and thus ensure that the process is performed in a safe thread,
which is recommended every time we are adding, removing, or modifying
an object.
The process to add a new book begins with the creation of the new object
with the Books() initializer. This not only creates a new object of type Books
but it also adds it to the context specified by the argument (dbContext). The
next step is to assign the values to the object's properties. We assign the
value of the first input field to the title property, the value of the second
input field to the year property, the images bookcover and bookthumbnail
to the cover and thumbnail properties, respectively (we assign standard
images to every book for now), and the nil value to the author property (we
still don't have an Authors object to associate with this book).
The Books() initializer inserts the new object into the context, but this
change is not permanent. If we close the app after the values are assigned
to the properties, the object is lost. To persist the changes, we must save
the context with the save() method. This should be done every time we
finish a process that modifies the context. The method takes the
information in the context and updates the Persistent Store with it, so
everything is stored permanently on file.
IMPORTANT: Core Data classes are created when the app is built.
Sometimes, Xcode can't recognize the classes created from the
entities and returns an error. One way to remove the errors is to
build the application by pressing the Command + B keys. If this
doesn't solve the issues, you can try running the application on the
simulator.
There are no view models in Core Data. The objects stored in the Persistent
Store (Books and Authors in our example), represent the application's data
model. They store the values and return them as they are. But the views
need the values to be formatted or casted before showing them on the
screen. For instance, in the RowBook view in Listing 3, we had to process the
value of the thumbnail property with a computed property to turn it into an
UIImage view. We also had to use the nil-coalescing operator to show a
string if there was no value in the title and author properties. And we even
had to cast the value of the year property to a string with the String()
initializer. All this work should not be done by the view, it should be done
by a view model. To create a view model for the Core Data objects, we can
extend the classes defined by the system. For example, we can create an
extension of the Books class to provide computed properties that always
return String values for the views to display, and process the images in the
thumbnail and cover properties, so we don't have to do it inside the view.
extension Books {
var showTitle: String {
return title ?? "Undefined"
}
var showYear: String {
return String(year)
}
var showAuthor: String {
return author?.name ?? "Undefined"
}
var showCover: UIImage {
if let data = cover, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
var showThumbnail: UIImage {
if let data = thumbnail, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
}
extension Authors {
var showName: String {
return name ?? "Undefined"
}
}

Extensions have access to the properties defined by the data type. This
means that we can access those properties, process their values, and
return something else. The example in Listing 5 defines an extension for
the Books class and an extension for the Authors class. The extensions include
computed properties that format the values in the original properties and
return a value for the views to display. In the extension of the Books class,
we also include a property that returns a string with the name of the
author, so the views don't have to read this value from the Authors object
anymore, they can do it directly from the Books object, as shown next.
The Authors objects are generated and stored the same way as the Books
objects. This demands our application to provide new views where the
user can select and add more objects. For our example, we have decided to
expand our interface with a view that lists the authors already inserted by
the user and another view to insert new ones.
The view on the left is the InsertBookView view introduced before to insert
new books. This view now shows three input options, an input field to
insert the title of the book, another to insert the year, and the Select
Author button to select the author (Figure 16, left). This button is a
NavigationLink view that opens a view to list all the authors available (Figure
16, center). In turn, this view includes a + button in the navigation bar to
open a view that includes an input field to insert the name of an author
(Figure 16, right). The first step we need to take to create this interface is to
add the Select Author button to the InsertBookView view, as shown below.
Every time an author is selected or created, we must get its Authors object
and send it back to the InsertBookView view to assign it to the book. To store
this value, the view includes a @State property called selectedAuthor. If the
property contains an Authors object, we show the value of its name property
to the user, otherwise, we show the text "Undefined". (Notice that in this
case we don't use the properties from the view models created before
because the selectedAuthor property is an optional, so we have to check its
value anyway.) Below the name, we include a NavigationLink button to open
a view called AuthorsView to let the user select an author.
This view lists the authors already inserted by the user. The @FetchRequest
property is called listOfAuthors and it is defined to work with Authors objects,
but other than that, the rest of the code is the same we used to list books.
The only significant difference is that the rows now include the
onTapGesture() modifier to let the user select an author. When the user taps
on the name of an author, we assign the Authors object to a @Binding
property called selected and close the view. Because the selected property is
connected to the selectedAuthor property defined in the InsertBookView view,
the name of the author selected by the user will be shown on the screen.
If there are no authors in the Persistent Store yet, or the author the user is
looking for is not on the list, the user can press a button in the navigation
bar that opens the InsertAuthorView view to insert a new author.
This is a simple view. It includes one TextField view to insert the name of the
author and a button to save it. When the button is pressed, we follow the
same procedure as before. The Authors object is initialized and stored in the
context, the name inserted by the user is assigned to the object's name
property, and the context is saved to make the changes permanent.
With these additions, our basic app is complete. Now authors can be
assigned to new books. When we pressed the Select Author button, the
app opens a view with all the authors available (Figure 16, center). If there
are no authors yet or the author we want is not on the list, we can press
the + button to insert a new one (Figure 16, right). Every time we select an
author from the list, the app goes back to the view with the book’s
information and shows the name of the selected author on the screen
(Figure 16, left). The Authors object that represents the author is assigned to
the book’s author property and therefore the name of the author is now
shown on the list of books.
This model creates the Persistent Store as before, but it also includes a
type property called preview to create a Persistent Store in memory for the
previews. The closure assigned to this property initializes the ApplicationData
object with the value true. The initializer checks this value and assigns a URL
structure with a null URL to the url property of the NSPersistentStoreDescription
object returned by the persistentStoreDescriptions property of the
NSPersistentContainer class. The NSPersistentStoreDescription object is used by Core
Data to create and load the Persistent Store, so when we assign a URL with
the "/dev/null" path to this object, the Persistent Store is created in
memory.
Now that we have a type property to create the Persistent Store for
previews, we can use it in the PreviewProvider structures. All we need to do is
to inject it into the environment as we did in the App structure, but this
time we access the viewContext property of the NSPersistentContainer object
returned by the ApplicationData object created by the preview property, as
shown next.
Listing 11: Accessing the Core Data context from the preview

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, ApplicationData.preview.container.viewContext)
}
}

Objects returned from a request are usually in the order they were
created, but this is not guaranteed. Foundation defines two data types to
sort objects, the NSSortDescriptor class to define NSFetchRequest objects, and
the SortDescriptor structure, designed to work with the @FetchRequest property
wrapper. The most useful in SwiftUI applications is the SortDescriptor
structure. From these structure, we can specify an order according to the
values of a property. The structure includes the following initializers.
If later we need to change the order of the objects, we can assign new
SortDescriptor structures to the FetchedResults structure created by the
@FetchRequest property wrapper. For this purpose, the structure includes the
property. In the following example, we add a button to the
sortDescriptors
navigation bar that opens a menu with three options to sort the values.

Predicates
The requests performed in previous examples are getting all the objects
associated to a particular entity and the values of all their properties. The
Core Data framework defines a class called NSPredicate to fetch only the
objects that pass a filter. For example, we could get only the books that
were published in the year 1983 or the authors which names start with
"Stephen". The class defines the following initializer to create a predicate
with all the conditions we need.
Of course, these are predefined conditions, but we can allow the user to
insert a value and use new predicates to perform a search. For this, we
need to replace the current predicate with a new one configured with the
new values we want to filter. To assign a predicate to the request, the
FetchedResults structure includes the nspredicate property. The following
example shows how to implement this property and define new predicates
to search for books by year.
Listing 18: Creating new predicates to search values in the Persistent Store

struct ContentView: View {
@FetchRequest(sortDescriptors: [], predicate: nil, animation: .default) var listOfBooks:
FetchedResults<Books>
@State private var search: String = ""
Predicates use comparison and logical operators like those offered by Swift.
For example, we can compare values with the operators =, !=, >, <, >= and
<=, and also concatenate conditions with the characters && (or the word
AND), || (or the word OR) and ! (or the word NOT). Predicates also include
keywords for a more meticulous search. The following are the most
frequently used.
In our example, the objects retrieved from the Persistent Store are stored
in the listOfBooks property. The indexes received by the closure in the
onDelete() modifier are the indexes of the objects the user wants to delete
from this collection. Therefore, to remove the objects selected by the user,
we iterate through the indexes of all the objects to be removed with a for in
loop, get the object from the listOfBooks collection using each index, and
remove it from the context with the delete() method. Once the objects are
removed from the context, the listOfBooks property is automatically updated
and the changes are reflected on the screen. The last step is to save the
context with the save() method to modify the Persistent Store with the
changes in the context, as we did before. (Notice that in this example we
had to get a direct reference to the context from the environment with the
@Environment property wrapper.)
Modifying an object in the Persistent Store is easy. When the user selects a
row, we pass the object to a view that provides input fields to modify its
values, assign the values inserted by the user to the object, and save the
context to make the changes permanent. First, we must add the navigation
link in the ContentView view to allow the user to select a book.
In this example, we apply the id() modifier to the RowBook view. We have
introduced this modifier before. It assigns a unique identifier to each view.
In this case, we use a new UUID value, which means that every time the
view is recreated, the identifier will be different. This makes the system
believe that this is a different view and therefore it updates the content,
showing on the screen the new values inserted by the user.
The NavigationLink view opens a view called ModifyBookView to allow the user
to modify the values of the selected book. The following is our
implementation of this view.
This view defines a constant called book to receive the Books object that
represents the book selected by the user. Because the user needs to see
the book to be able to modify it, we implement the onAppear() modifier to
initialize state properties with the values in the book property. Notice that
there is an additional property called valuesLoaded that we check to only load
the values when the property is false. This is to make sure that the values
are only loaded from the Persistent Store when the user selected a book
from the ContentView view, but not when a new author has been selected
from the AuthorsView view.
The view includes two TextField views for the user to be able to modify the
title and the year, and the same Select Author button included before to
open the AuthorsView view for the user to select an author. When the Save
button is pressed, the values inserted by the user are assigned to the
object's properties and the context is saved.
In this opportunity, we do not create a new Books object, we just modify the
properties of the Books object received by the view. The result is shown
below.
So far, we have let the @FetchRequest property wrapper create the request
for us, but there are situations in which we must create our own requests
to process the values in the Persistent Store. Because a request has to be
associated to an entity, subclasses of the NSManagedObject class, like Books
and Authors, include the fetchRequest() method. This method returns an
NSFetchRequest object with a fetch request associated to the entity. To
perform this request, the context includes the fetch() method. The following
example creates a request when the view appears on the screen and when
an object is removed to count the number of books in the Persistent Store
and show the value at the top of the list.
ForEach(listOfBooks) { book in
NavigationLink(destination: ModifyBookView(book: book), label: {
RowBook(book: book)
.id(UUID())
})
}
.onDelete(perform: { indexes in
for index in indexes {
dbContext.delete(listOfBooks[index])
countBooks()
}
do {
try dbContext.save()
} catch {
print("Error deleting objects")
}
})
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: InsertBookView(), label: {
Image(systemName: "plus")
})
}
}
.onAppear {
countBooks()
}
}
}
func countBooks() {
let request: NSFetchRequest<Books> = Books.fetchRequest()
if let list = try? self.dbContext.fetch(request) {
totalBooks = list.count
}
}
}

To store the number of books, this view defines a @State property called
totalBooks, and to show the value, it includes an HStack with two Text views on
top of the list. The request is created by a method called countBooks(). The
method is called when the view appears and when an object is removed. It
creates a request for the Books entity, and then executes the request in the
context with the fetch() method. If there are no errors, this method returns
an array with all the objects that match the request. In this case, we didn't
define any predicate, so the array contains all the Books objects in the
Persistent Store. Finally, we count the objects with the count property and
assign the value to the totalBooks property to show it to the user.
Figure 20: Total number of books in the Persistent Store
This view includes a Text view that shows the total number of books next to
the author's name. To get this value, we count the number of items in the
books property or show the value 0 if the property is equal to nil (no books
have been assigned to the author).
The code in Listing 27 creates a request for the Authors entity and uses the
value inserted in the TextField view to create a predicate. The predicate
looks for objects with the name equal to the value of the newName constant.
Using this request, we call the count() method in the context to get the total
amount of objects that match the conditions in the predicate. If the value
returned is 0, we know that there are no authors in the Persistent Store
with that name and we can proceed.
Now, when a book is selected, the app opens the AuthorBooksView view with
a reference to the Authors object that represents the author selected by the
user. In this view, we must use the reference to create a dynamic fetch
request, as shown next.
init(selectedAuthor: Authors?) {
self.selectedAuthor = selectedAuthor
if selectedAuthor != nil {
_listOfBooks = FetchRequest(sortDescriptors: [SortDescriptor(\Books.title, order:
.forward)], predicate: NSPredicate(format: "author = %@", selectedAuthor!), animation:
.default)
}
}
var body: some View {
List {
ForEach(listOfBooks) { book in
Text(book.title ?? "Undefined")
}
}
.navigationBarTitle(selectedAuthor?.name ?? "Undefined")
}
}
struct AuthorBooksView_Previews: PreviewProvider {
static var previews: some View {
AuthorBooksView(selectedAuthor: nil)
}
}

The view receives a reference to the Authors object that represent the
author selected by the user, but we can't use this value in the @FetchRequest
property wrapper because the property is not available until the whole
structure is initialized. We must define a basic request and then assign a
new FetchRequest structure to the property wrapper from the initializer with
the predicate we need to only get the books published by the selected
author.
To access the underlining structure of the property wrapper, we must
precede the property name with an underscore (_listOfBooks). In this
example, we initialize the selectedAuthor property, and then assign a new
FetchRequest structure to the property wrapper with a predicate that filters
the books by the author assigned to that property. The result is shown
below.
SectionedFetchRequest(sectionIdentifier: KeyPath,
sortDescriptors: [SortDescriptor], predicate: NSPredicate?,
animation: Animation?)—This initializer creates a request with the
configuration determined by the arguments and produces a
SectionedFetchResults structure that manages and delivers the objects to
the view. The sectionIdentifier argument is a key path to the property
that the request is going to use to create the sections, the
sortDescriptors argument is an array of SortDescriptor structures that
determine the order of the objects, the predicate argument is an
NSPredicate object that filters the objects, and the animation argument
determines how the changes are going to be animated.
SectionedFetchRequest(fetchRequest: NSFetchRequest,
sectionIdentifier: KeyPath, animation: Animation?)—This
initializer creates a request with the NSFetchRequest object provided by
the fetchRequest argument and produces a SectionedFetchResults
structure that manages and delivers the objects to the view. The
sectionIdentifier argument is a key path to the property that the
request is going to use to create the sections, and the animation
argument determines how the changes are going to be animated.
In this example, we use the name property of the Authors entity to identify
the sections, so one section is created per author. Because a book may not
have an associated author, the author property of the Books object may
return nil, so the value used to identify the section is an optional String.
That's the reason why we declare the SectionedFetchResults data types as
<String?, Books>.
To present the data in sections, we need to define a ForEach loop for the
sections, and another for the objects in each section. The sections are
identified with a Section view, and the section's label is created with a Text
view from the value returned by the id property (in our example, this is the
author's name).
When the user presses the Save button to save the book, we read the first
property to get the letter in the string and make sure that is an uppercase
letter. If the letter is actually a number, we replace it with a # character. (All
books which titles begin with a number will be listed in a unique section
identified with the # character.) Finally, the letter is stored in the firstLetter
property and the book is saved.
We need to perform the same process when a book is modified. The
following are the changes we need to introduce to the saveBook() method in
the ModifyBookView view.
do {
try dbContext.save()
dismiss()
} catch {
print("Error saving record")
}
}
}

Now that every book knows the letter it belongs to, we can list them in
alphabetical sections. The following is the new @SectionedFetchRequest
property wrapper we need for the ContentView view.
Now the section identifier is the value of the firstLetter property and the
books are sorted by title. The result is shown below.
The previous examples assumed that there was only one author per book,
but sometimes multiple authors collaborate to write a book. To assign
multiple Authors objects to a book, we must turn the author relationship of
the Books entity into a To-Many relationship, as shown below.
Now, both relationships are of type To-Many, which means we can assign
multiple books to an author and multiple authors to a book. This
introduces a problem. Before, every time we wanted to assign an author to
a book, we just had to create a new Authors object and assign it to the
book's author property. Core Data took care of adding the book to the books
property of the Authors object, along with the rest of the books associated
to that author. But we can't do that when both relationships are To-Many.
In that case, we must read and write the values ourselves.
The values of a To-Many relationship are stored in an NSSet object. This is a
class defined by the Foundation framework to store sets of values. To read
the values in an NSSet, we can cast it as a Swift set, but to turn a Swift set or
an array into an NSSet object, we must implement the following initializers.
In our application, the first place we need to read these values is in the
view model (the Extensions.swift file). We must create a property that
returns a string with the list of authors separated by comma.
Listing 34: Creating a string with the name of the authors from the view
model

import SwiftUI
extension Books {
var showTitle: String {
return title ?? "Undefined"
}
var showYear: String {
return String(year)
}
var showAuthors: String {
var authors: String!
if let list = author as? Set<Authors> {
let listNames = list.map({ $0.name ?? "Undefined" })
if !listNames.isEmpty {
authors = listNames.joined(separator: ", ")
}
}
return authors ?? "Undefined"
}
var showCover: UIImage {
if let data = cover, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
var showThumbnail: UIImage {
if let data = thumbnail, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
}
extension Authors {
var showName: String {
return name ?? "Undefined"
}
}

The showAuthors property in this view model replaces the showAuthor property
implemented before. To create the list of names, we cast the value of the
author property to a Set<Authors> value. This creates a Swift set with Authors
objects representing all the authors assigned to the book, so we can map
the values into an array of strings and call the joined() method to create a
single string with the names separated by comma.
Now, we can show all the authors of a book on the list by reading the
showAuthors property, as shown next.
The next step is to allow the users to associate multiple authors with a
book. The following are the changes we must introduce to the
InsertBookView view to allow the user to select multiple authors when a new
book is added.
do {
try dbContext.save()
dismiss()
} catch {
print("Error saving record")
}
}
}
}

The values are now stored in an array, so we always know which ones were
selected by the user. To show the list of authors, we define a computed
property called showAuthors that follows the same process performed by the
view model; it maps the Authors objects and returns a string with the names
separated by comma. The view includes a Text view that reads this property
and shows the names on the screen.
When the user decides to save the book, we perform the inverse
procedure. The values in the selectedAuthors array are stored in an NSSet
object and assigned to the author property.
To allow the user to select the authors when a book is modified, we must
also apply these changes to the ModifyBookView view, as shown next.
Listing 37: Modifying the authors of a book

struct ModifyBookView: View {
@Environment(\.managedObjectContext) var dbContext
@Environment(\.dismiss) var dismiss
@State private var selectedAuthors: [Authors] = []
@State private var inputTitle: String = ""
@State private var inputYear: String = ""
@State private var valuesLoaded: Bool = false
do {
try dbContext.save()
dismiss()
} catch {
print("Error saving record")
}
}
}
}

This view is very similar to the InsertBookView view, the only difference is a
new Boolean @State property called changesAdded that we use to know if the
user is coming from the ContentView view or the AuthorsView view. If the user
opened this view from the ContentView view, we need to load the list of
authors from the Persistent Store and assign them to the selectedAuthors
property when the view appears. Otherwise, the content of the
selectedAuthors property is determined by the authors selected by the user in
the AuthorsView view. Next are the changes we need to introduce to this
view.
As always, this view displays all available authors. To show which one has
been previously selected, we add an Image view with a checkmark to the
row of the authors that are already in the selectedAuthors array. Then, when a
row is tapped by the user, we check whether the author was previously
selected or not. If it was selected, we remove it from the selectedAuthors
array, otherwise, we add it to the array. This makes sure that the array only
contains the authors currently selected by the user.
Do It Yourself: Open the Core Data model. Select the Books entity
and change the Type of the author relationship to To-Many (Figure
25). Update the Extensions.swift file with the code in Listing 34, the
ContentView view with the code in Listing 35, the InsertBookView with the
code in Listing 36, the ModifyBookView view with the code in Listing 37,
and the AuthorsView view with the code in Listing 38. Run the
application on the iPhone simulator. Press the + button to add a new
book. Press the Select Authors button to select an author. You should
be able to add as many authors as you want.
The To-Many to To-Many relationships also change the way we search for
values. For instance, we cannot search for a book by author as we did
before because now a book may be associated to many authors. Instead,
we must tell the predicate to search for the value inside the set of authors.
For this purpose, predicates can include the following keywords.
ANY—This keyword returns true when the condition is true for some
of the values in the set.
ALL—This keyword returns true when the condition is true for all the
values in the set.
NONE—This keyword returns true when the condition is false for all
the values in the set.
The example we have been working on so far turns the NSSet object
returned by the author relationship into an array of Authors objects and
then adds or removes authors from this array, but if we need to add or
remove values directly from the NSSet object, we must turn it into an
NSMutableSet object. This class creates a mutable set and therefore it allows
us to add or remove values from it. To create an NSMutableSet object from an
NSSet object, the NSManagedObject class includes the following method.
The NSMutableSet class includes the following methods to add and remove
items in the set.
Task(priority: .high) {
await dbContext.perform {
for book in listOfBooks {
let authorSet = book.mutableSetValue(forKey: "author")
authorSet.remove(author)
book.author = authorSet
}
try? dbContext.save()
}
}
}
}

This example updates the onAppear() modifier defined before to modify the
Books objects in the Persistent Store as soon as the view is loaded. First, we
perform a request to get the Authors object with the name "Stephen King".
Then, we use a for in loop to modify the books loaded by the @FetchRequest
property wrapper. In the loop, we turn the NSSet object returned by the
author relationship into an NSMutableSet object, remove from the set the
Authors object fetched before with the remove() method, and assign the
result back to the author relationship, effectively removing that author
from every book.
Do It Yourself: Update the onAppear() modifier with the code in
Listing 40. Run the application on the iPhone simulator. The author
Stephen King should be removed from every book.
Find more books at
www.formasterminds.com
J.D Gauchat
www.jdgauchat.com