The preceding chapters covered the concepts of database storage using the SQLite database. In these chapters, the assumption was made that the iOS app code would directly manipulate the database using SQLite API calls to construct and execute SQL statements. While this is a good approach for working with SQLite in many cases, it does require knowledge of SQL and can lead to some complexity in terms of writing code and maintaining the database structure. The non-object-oriented nature of the SQLite API functions further compounds this complexity. In recognition of these shortcomings, Apple introduced the Core Data Framework. Core Data is essentially a framework that places a wrapper around the SQLite database (and other storage environments), enabling the developer to work with data in terms of Swift objects without requiring any knowledge of the underlying database technology.
We will begin this chapter by defining some concepts that comprise the Core Data model before providing an overview of the steps involved in working with this framework. Once these topics have been covered, the next chapter will work through An iOS 17 Core Data Tutorial.
The Core Data Stack
Core Data consists of several framework objects that integrate to provide the data storage functionality. This stack can be visually represented as illustrated in Figure 48-1.
As shown in Figure 48-1, the iOS-based app sits on top of the stack and interacts with the managed data objects handled by the managed object context. Of particular significance in this diagram is that although the lower levels in the stack perform a considerable amount of the work involved in providing Core Data functionality, the app code does not interact with them directly.
Before moving on to the more practical areas of working with Core Data, it is essential to explain the elements that comprise the Core Data stack in a little more detail.
Persistent Container
The persistent container handles the creation of the Core Data stack and is designed to be easily subclassed to add additional app-specific methods to the base Core Data functionality. Once initialized, the persistent container instance provides access to the managed object context.
Managed Objects
Managed objects are the objects that are created by your app code to store data. For example, a managed object may be considered a row or a record in a relational database table. For each new record to be added, a new managed object must be created to store the data. Similarly, retrieved data will be returned as managed objects, one for each record matching the defined retrieval criteria. Managed objects are actually instances of the NSManagedObject class or a subclass thereof. These objects are contained and maintained by the managed object context.
Managed Object Context
Core Data based apps never interact directly with the persistent store. Instead, the app code interacts with the managed objects contained in the managed object context layer of the Core Data stack. The context maintains the status of the objects in relation to the underlying data store and manages the relationships between managed objects defined by the managed object model. All interactions with the underlying database are held temporarily within the context until the context is instructed to save the changes. At this point, the changes are passed down through the Core Data stack and written to the persistent store.
Managed Object Model
So far, we have focused on managing data objects but have not yet looked at how the data models are defined. This is the task of the Managed Object Model, which defines a concept referred to as entities.
Much as a class description defines a blueprint for an object instance, entities define the data model for managed objects. Essentially, an entity is analogous to the schema that defines a table in a relational database. As such, each entity has a set of attributes associated with it that define the data to be stored in managed objects derived from that entity. For example, a Contacts entity might contain name, address, and phone number attributes.
In addition to attributes, entities can also contain relationships, fetched properties, and fetch requests:
- Relationships – In the context of Core Data, relationships are the same as those in other relational database systems in that they refer to how one data object relates to another. Core Data relationships can be one-to-one, one-to-many, or many-to-many.
- Fetched property – This provides an alternative to defining relationships. Fetched properties allow properties of one data object to be accessed from another as though a relationship had been defined between those entities. Fetched properties lack the flexibility of relationships and are referred to by Apple’s Core Data documentation as “weak, one-way relationships” best suited to “loosely coupled relationships.”
- Fetch request – A predefined query that can be referenced to retrieve data objects based on defined predicates. For example, a fetch request can be configured into an entity to retrieve all contact objects where the name field matches “John Smith.”
Persistent Store Coordinator
The persistent store coordinator coordinates access to multiple persistent object stores. As an iOS developer, you will never directly interact with the persistence store coordinator. In fact, you will very rarely need to develop an app that requires more than one persistent object store. When multiple stores are required, the coordinator presents these stores to the upper layers of the Core Data stack as a single store.
Persistent Object Store
The term persistent object store refers to the underlying storage environment in which data are stored when using Core Data. Core Data supports three disk-based and one memory-based persistent store. Disk-based options consist of SQLite, XML, and binary. By default, the iOS SDK will use SQLite as the persistent store. In practice, the type of store used is transparent to you as the developer. Regardless of your choice of persistent store, your code will make the same calls to the same Core Data APIs to manage the data objects required by your app.
Defining an Entity Description
Entity descriptions may be defined from within the Xcode environment. For example, when a new project is created with the option to include Core Data, a template file will be created named <projectname>.xcdatamodeld. Selecting this file in the Xcode project navigator panel will load the model into the entity editing environment, as illustrated in Figure 48-2:
Create a new entity by clicking on the Add Entity button located in the bottom panel. The new entity will appear as a text box in the Entities list. By default, this will be named Entity. Double-click on this name to change it.
To add attributes to the entity, click the Add Attribute button in the bottom panel or use the + button beneath the Attributes section. Then, in the Attributes panel, name the attribute and specify the type and any other required options.
Repeat the above steps to add more attributes and additional entities.
The Xcode entity environment also allows relationships to be established between entities. Assume, for example, two entities named Contacts and Sales. First, select the Contacts entity and click on the + button beneath the Relationships panel to establish a relationship between the two tables. Then, in the detail panel, name the relationship, specify the destination as the Sales entity, and any other options required for the relationship.
As demonstrated, Xcode makes the process of entity description creation reasonably straightforward. While a detailed overview of the process is beyond this book’s scope, many other resources are dedicated to the subject.
Initializing the Persistent Container
The persistent container is initialized by creating a new NSPersistentContainer instance, passing through the name of the model to be used, and then making a call to the loadPersistentStores method of that object as follows:
let container = NSPersistentContainer(name: "CoreDataDemo")
container.loadPersistentStores(completionHandler: {
(description, error) in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
})
Code language: Swift (swift)
Obtaining the Managed Object Context
Since many Core Data methods require the managed object context as an argument, the next step after defining entity descriptions often involves obtaining a reference to the context. This can be achieved by accessing the viewContext property of the persistent container instance:
let managedObjectContext = persistentContainer.viewContext
Code language: Swift (swift)
Getting an Entity Description
Before managed objects can be created and manipulated in code, the corresponding entity description must first be loaded. This is achieved by calling the entity(forName:in:) method of the NSEntityDescription class, passing through the name of the required entity and the context as arguments. For example, the following code fragment obtains the description for an entity with the name Contacts:
let entity = NSEntityDescription.entity(
forName: "Contacts", in: context)
Code language: Swift (swift)
Setting the Attributes of a Managed Object
As previously discussed, entities and the managed objects from which they are instantiated contain data in the form of attributes. Once a managed object instance has been created, as outlined above, those attribute values can store the data before the object is saved. For example, assuming a managed object named contact with attributes named name, address, and phone, respectively, the values of these attributes may be set as follows before the object is saved to storage:
contact.name = "John Smith"
contact.address = "1 Infinite Loop"
contact.phone = "555-564-0980"
Code language: Swift (swift)
Saving a Managed Object
Once a managed object instance has been created and configured with the data to be stored, it can be saved to storage using the save method of the managed object context as follows:
do {
try context.save()
} catch let error {
// Handle error
}
Code language: Swift (swift)
Fetching Managed Objects
Once managed objects are saved into the persistent object store, those objects and the data they contain will likely need to be retrieved. Objects are retrieved by executing a fetch request and are returned in an array. The following code assumes that both the context and entity description have been obtained before making the fetch request:
let request: NSFetchRequest<Contacts> = Contacts.fetchRequest()
request.entity = entity
do {
let results = try context.fetch(request as!
NSFetchRequest<NSFetchRequestResult>)
} catch let error {
// Handle error
}
Code language: Swift (swift)
Upon execution, the results array will contain all the managed objects retrieved by the request.
Retrieving Managed Objects based on Criteria
The preceding example retrieved all managed objects from the persistent object store for a specified entity. More often than not, only managed objects that match specified criteria are required during a retrieval operation. This is performed by defining a predicate that dictates criteria a managed object must meet to be eligible for retrieval. For example, the following code implements a predicate to extract only those managed objects where the name attribute matches “John Smith”:
let request: NSFetchRequest<Contacts> = Contacts.fetchRequest()
request.entity = entity
let pred = NSPredicate(format: "(name = %@)", "John Smith")
request.predicate = pred
do {
let results = try context.fetch(request as!
NSFetchRequest<NSFetchRequestResult>)
} catch let error {
// Handle error
}
Code language: Swift (swift)
Accessing the Data in a Retrieved Managed Object
Once results have been returned from a fetch request, the data within the returned objects may be accessed using keys to reference the stored values. The following code, for example, accesses the first result from a fetch operation results array and extracts the values for the name, address, and phone keys from that managed object:
let match = results[0] as! NSManagedObject
let nameString = match.value(forKey: "name") as! String
let addressString = match.value(forKey: "address") as! String
let phoneString = match.value(forKey: "phone") as! String
Code language: Swift (swift)
Summary
The Core Data Framework stack provides a flexible alternative to directly managing data using SQLite or other data storage mechanisms. Providing an object-oriented abstraction layer on top of the data makes managing data storage significantly easier for the iOS app developer. Now that the basics of Core Data have been covered, the next chapter, entitled An iOS 17 Core Data Tutorial, will work through creating an example app.