Core Data Fundamentals: Stack and Model

Understand Core Data's architecture from application code down to the store file — the managed object model, persistent store coordinator, persistent container, and managed object context — and learn how entities, attributes, relationships, fetch requests, predicates, and sort descriptors let you model and query an object graph.

Core Data is Apple's object-graph management and persistence framework, and it's a considerably higher-level way to store data than the raw SQLite C API from the previous lesson — in fact, on most Apple platforms, Core Data's default persistent store is a SQLite database file, meaning Core Data is, in a real sense, built on top of exactly what you just learned. Where SQLite hands you tables, rows, and SQL text, Core Data hands you an object graph: Objective-C objects representing your entities, with attributes and relationships, that you fetch, mutate, and save without writing SQL by hand at all. Core Data's own machinery translates your object-graph operations into the underlying store's actual read/write operations. Core Data's concept list is unusually large, so it's split across two lessons sharing this module: this lesson establishes the stack — the pieces that sit between your application code and the store file, and the vocabulary for modeling your data — while the next lesson covers what happens when you actually save, how changes propagate and merge, how to use Core Data safely across threads, and how to keep it fast.

The Core Data Stack, Top to Bottom

Core Data's architecture is a small stack of cooperating objects, each with a distinct, narrow responsibility, and understanding what each layer does — and, just as importantly, what it does not do — is the single most valuable mental model for working with the framework. At the top sits your application code, which never talks to a database file directly. Below that sits the managed object context, your working area for fetching and mutating objects. Below the context sits the persistent store coordinator, which mediates between your object model and the actual store file(s) on disk. And at the bottom sits the store itself — by default, a SQLite database file, though Core Data supports other store types too.

In modern code, you rarely assemble these pieces individually — NSPersistentContainer bundles the model, the coordinator, and the default store's setup into one convenience object that you configure and load once, typically at app launch. Older code (and a good deal of code still in production) predates NSPersistentContainer and wires up an NSManagedObjectModel, NSPersistentStoreCoordinator, and NSManagedObjectContext by hand; understanding each layer individually, as this lesson does, means you can read either style comfortably and know exactly what NSPersistentContainer is doing on your behalf underneath its convenience API.

This lesson's diagram lays out that whole chain in order — from your application, through the persistent container, into a managed object context, through to the persistent store, and finally down to the actual SQLite (or other) file on disk. Every concept covered in this lesson is one link in that same chain, and keeping the diagram in mind while reading each section is the fastest way to see how the individual pieces fit together into one coherent system rather than a list of unrelated class names to memorize.

The Managed Object Model: Your Schema

NSManagedObjectModel is Core Data's representation of your schema — the complete description of every entity your app knows about, each entity's attributes and their types, and the relationships between entities. In an Xcode project, this schema is typically authored visually in a .xcdatamodeld file — a graphical entity-relationship editor — rather than written out by hand in code, though it's entirely possible to construct an NSManagedObjectModel programmatically if you need to generate a schema at runtime.

The model is loaded once, early in your app's life (NSPersistentContainer does this for you automatically from your .xcdatamodeld bundle resource), and from that point on it's the single source of truth every other layer of the stack consults: the persistent store coordinator uses it to know what tables/columns the underlying store needs, and every managed object instance you work with is, at its core, an instance shaped by an entity description drawn from this model.

Because the model is a static description loaded from a compiled resource, changing it after you've shipped a version of your app to users — adding a new attribute, splitting an entity, changing a relationship's cardinality — is exactly the migrations problem covered in the next lesson: the model describes the schema your app's current code expects, and if a user's existing store file was built against an older model, something has to reconcile the two before that store can be opened successfully.

The Persistent Store Coordinator: Mediator Between Model and Store

NSPersistentStoreCoordinator sits between the managed object model and the actual store file(s), and its job is to mediate: it knows the schema (from the model) and it knows how to translate operations against that schema into whatever the underlying store technology actually requires — for the default SQLite store, that means translating object-graph fetches, inserts, and saves into the same kind of SQL operations you wrote by hand in the previous lesson, just generated by Core Data instead of by you.

A single coordinator can, in principle, manage more than one persistent store simultaneously — an advanced configuration used, for instance, to split some entities into one file and others into a separate file, or to layer a read-only bundled store underneath a writable one. Most apps use exactly one store through their coordinator, but the architecture supports more, which is part of why the coordinator exists as a distinct layer rather than being folded directly into the store or the context.

You rarely interact with the coordinator directly in modern code, because NSPersistentContainer creates and configures it for you as part of loading your persistent stores. It's still worth knowing it's there and what it does, because error handling around store loading — a corrupted store file, a store that needs migration and can't be opened automatically — surfaces through the coordinator's store-loading APIs, which NSPersistentContainer's loadPersistentStores: method wraps and reports back to you.

The Persistent Container: The Modern Entry Point

NSPersistentContainer is the class you actually interact with when setting up Core Data in current code. You create one with a name matching your .xcdatamodeld file ([[NSPersistentContainer alloc] initWithName:@"MyModel"]), then call loadPersistentStores: with a completion handler, which asynchronously (though it can complete synchronously for a local, already-created store) creates the coordinator, loads the model, and opens or creates the store file, reporting any error back to you rather than crashing your app outright — checking that error is not optional, because a failure here typically means Core Data isn't usable at all until it's resolved.

Once loading succeeds, the persistent container exposes a ready-to-use viewContext — a managed object context configured to run on the main queue, intended for UI-facing work — as well as a newBackgroundContext method for creating additional contexts tied to their own private background queue, which the next lesson covers in the context of concurrency and background import work.

NSPersistentContainer intentionally bundles what used to be several lines of manual setup (create a model, create a coordinator, add a store to the coordinator, create a context, set the context's coordinator) into one object with a small, convenient surface area. It doesn't remove any of the underlying layers — the model, coordinator, and store are all still there, and still work exactly as described in the previous two sections — it just assembles and configures them for you, and gives you a shorter path to a working viewContext you can start fetching and saving through.

The Managed Object Context: Your Scratchpad

NSManagedObjectContext is where almost all of your actual day-to-day work with Core Data happens. Think of a context as a scratchpad: you fetch objects into it, create new objects in it, modify objects' attributes while they live in it, and none of those changes reach the persistent store until you explicitly call save: on that context. Until that save happens, all of your inserts, updates, and deletes exist only in the context's in-memory scratchpad — you can freely experiment, and if you decide not to keep a set of changes, you can discard them (with rollback or by simply not saving) rather than committing them.

Every managed object — every instance of your entity classes that Core Data hands you — belongs to exactly one context at a time, and that context is what tracks the object's changes and is responsible for eventually persisting (or discarding) them. You cannot freely pass a managed object from one context to another and keep using it as-is; each context manages its own set of object instances, even when two contexts are looking at the same underlying row in the store, which is exactly why the next lesson's discussion of multiple contexts (concurrency, background contexts, merging) matters as much as it does.

A context is created against a particular concurrency type — NSMainQueueConcurrencyType for a context meant to be used from the main thread (like the persistent container's viewContext), or NSPrivateQueueConcurrencyType for a context that runs its own private background queue. This detail is central enough to Core Data's safe usage that the next lesson devotes an entire section to it; for now, the important fact is simply that a context is tied to a specific queue and its operations are meant to run on that queue, not accessed carelessly from anywhere.

Entities, Attributes, and Relationships: Modeling Your Object Graph

An entity is Core Data's term for what a class is to Objective-C — a named kind of thing your app models, like Task or User, defined in your managed object model. Each entity has a corresponding managed object class (by default, a dynamically generated subclass of NSManagedObject, or a class you generate/write yourself if you need custom behavior on top) that instances of that entity are represented by at runtime.

An attribute is a scalar or simple piece of data belonging to an entity — a string, a number, a date, a boolean, binary data — declared in the model with a name and a type, the same way you'd declare a property on an ordinary Objective-C class. A Task entity might have a title (string) attribute, a dueDate (date) attribute, and an isComplete (boolean) attribute; each managed object instance of that entity carries its own values for each declared attribute.

A relationship links one entity to another — a Task might have a to-one relationship to a Project it belongs to, while a Project has the inverse to-many relationship to all of its Tasks. Relationships in Core Data are always meant to be declared with an inverse on both sides (Core Data will warn you if you don't), because that inverse is what lets the framework keep both sides of the relationship consistent automatically — assign a Task's project relationship, and that task is automatically added to the Project's tasks to-many relationship, without you having to update both sides by hand.

• Entity — a modeled kind of object (roughly a class), like Task or Project. • Attribute — a scalar/simple field on an entity: string, number, date, boolean, binary data. • Relationship — a link between entities, to-one or to-many, normally declared with an inverse on both sides.

Fetch Requests: Querying the Object Graph

NSFetchRequest is how you ask a managed object context for objects matching some criteria — the Core Data equivalent of a SQL SELECT, expressed as an object graph query instead of SQL text. You create one targeting a specific entity ([NSFetchRequest fetchRequestWithEntityName:@"Task"]), optionally attach a predicate to filter results and sort descriptors to order them, and then execute it against a context with executeFetchRequest:error:, which returns an array of matching managed objects (or nil plus an error on failure).

A fetch request can be configured well beyond a bare 'give me everything': fetchLimit caps how many results come back, fetchBatchSize controls how results are paged in from the store as you access them (important for large result sets, covered further in the next lesson's performance section), and resultType can even be changed to fetch just object IDs, counts, or dictionaries of specific properties instead of full managed objects, when you don't need entire objects back.

Executing a fetch request always goes through the specific context you call it on, and the results it hands back are managed objects registered in that context — meaning that, once fetched, the objects behave exactly like any other objects living in that context: you can modify their attributes, and those changes are tracked by the context the same as changes to an object you inserted yourself, ready to be saved (or rolled back) later.

Predicates and Sort Descriptors: Filtering and Ordering

NSPredicate is Foundation's general-purpose condition language — it isn't exclusive to Core Data at all; you'll also see it used to filter plain NSArrays with filteredArrayUsingPredicate: — but it's central to how fetch requests express filtering criteria. A predicate is typically built with a format string similar in spirit to stringWithFormat:: [NSPredicate predicateWithFormat:@"age > %d", 18] builds a predicate matching objects whose age property is greater than 18, and you attach it to a fetch request's predicate property to have Core Data translate that condition down into the underlying store's actual query mechanism (a SQL WHERE clause, for the default SQLite store).

Predicates compose: [NSPredicate predicateWithFormat:@"isComplete == NO AND dueDate < %@", someDate] combines two conditions with AND, and Foundation supports OR, NOT, string-matching operators like BEGINSWITH/CONTAINS, and comparisons against related objects' properties through key paths (project.name == %@) — meaning a predicate can filter based on a relationship, not only an entity's own attributes.

NSSortDescriptor controls the order of a fetch request's results, independent of the predicate that controls which results come back at all — the two work together but do entirely different jobs. [NSSortDescriptor sortDescriptorWithKey:@"dueDate" ascending:YES] orders results by dueDate earliest-first; a fetch request's sortDescriptors property takes an array of these, applied in order, so you can sort by one key and use a second sort descriptor to break ties within the first.

At this point you have the full 'read side' of Core Data: how the stack is assembled from application code down to the store file, how to model an object graph with entities, attributes, and relationships, and how to query that graph with fetch requests, predicates, and sort descriptors. That's enough to load and display data, but it's deliberately incomplete — nothing here has covered what happens when you actually change something and need it to persist, or what happens when more than one context or more than one thread is involved. The next lesson, Core Data: Change Tracking, Concurrency, and Migration, picks up exactly there: faults, change tracking, saving, merging, concurrency, background contexts, migrations, and performance — including batch operations that update or delete many rows without loading each one into memory first.

Code Examples

This is the standard modern setup: create an NSPersistentContainer named after your .xcdatamodeld file, then call loadPersistentStores: with a completion handler. Under the hood this single call loads the managed object model, creates the persistent store coordinator, and opens (or creates) the store file — all three layers of the stack below your application code, assembled for you. The completion handler's error parameter must be checked; a non-nil error means the stack isn't usable yet, and ignoring it is a common source of confusing crashes later when code assumes viewContext is ready.

[self.persistentContainer loadPersistentStores:^(NSPersistentStoreDescription *description,

The fetch request targets the Task entity, filters with a predicate matching only incomplete tasks, and sorts the results by dueDate ascending — earliest due date first. executeFetchRequest:error: runs against viewContext, the main-queue context the persistent container hands you, and returns nil (with error populated) on failure rather than throwing, so checking for nil results is required, exactly the same NSError-out-parameter convention used throughout Foundation.

request.predicate = [NSPredicate predicateWithFormat:@"isComplete == NO"];

This creates a Project and a Task and links them through the Task entity's to-one project relationship. Because that relationship was declared in the managed object model with an inverse — Project's to-many tasks relationship — setting one side automatically updates the other; project.tasks reflects the new task without any separate call to add it there yourself. This automatic bookkeeping is one of the concrete conveniences an object-graph framework like Core Data provides over hand-written SQL foreign keys, where you would be responsible for keeping both sides of such a relationship consistent yourself. Expected result: Project now has 1 task(s)

@property (strong, nonatomic) NSPersistentContainer *persistentContainer;

- (void)setUpCoreDataStack {
    self.persistentContainer = [[NSPersistentContainer alloc] initWithName:@"MyModel"];

    [self.persistentContainer loadPersistentStores:^(NSPersistentStoreDescription *description,
                                                       NSError *error) {
        if (error != nil) {
            NSLog(@"Failed to load persistent store: %@, %@", error, error.userInfo);
            // A real app should handle this — a corrupted store, a failed migration —
            // rather than assuming Core Data is always ready to use.
            return;
        }
        NSLog(@"Core Data stack ready, store at: %@", description.URL);
    }];
}
- (NSArray<Task *> *)incompleteTasksSortedByDueDate {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Task"];
    request.predicate = [NSPredicate predicateWithFormat:@"isComplete == NO"];
    request.sortDescriptors = @[
        [NSSortDescriptor sortDescriptorWithKey:@"dueDate" ascending:YES]
    ];

    NSManagedObjectContext *context = self.persistentContainer.viewContext;
    NSError *error = nil;
    NSArray<Task *> *results = [context executeFetchRequest:request error:&error];

    if (results == nil) {
        NSLog(@"Fetch failed: %@", error);
        return @[];
    }
    return results;
}
Project *project = [NSEntityDescription insertNewObjectForEntityForName:@"Project"
                                                   inManagedObjectContext:context];
project.name = @"Redesign Website";

Task *task = [NSEntityDescription insertNewObjectForEntityForName:@"Task"
                                             inManagedObjectContext:context];
task.title = @"Draft new homepage layout";

task.project = project;
// Because "project" (on Task) and "tasks" (on Project) are declared as inverses
// of each other in the model, Core Data automatically adds "task" to
// project.tasks as well — you do not update both sides by hand.

NSLog(@"Project now has %lu task(s)", (unsigned long)project.tasks.count);
// Output: Project now has 1 task(s)

The Core Data Stack

Application code talks to a managed object context; the persistent container assembles the model, coordinator, and store beneath it; the persistent store is ultimately backed by a SQLite file (or another store type) on disk.

Application
    ↓
NSPersistentContainer
    ↓
Managed Object Context
    ↓
Persistent Store
    ↓
SQLite / Other Store

Why Apps Reach for Core Data Instead of Raw SQLite

Apps with a genuine object graph — interconnected entities, relationships that need to stay consistent, undo support, or data that's frequently displayed and edited through table/collection views — tend to reach for Core Data specifically because it removes the need to hand-write SQL, hand-manage foreign key consistency, and hand-track which objects changed since the last save. The tradeoff is real, though: Core Data has real behavioral rules of its own (concurrency, faulting, migration) that this and the next lesson exist to teach, and for an app whose data is simple, flat, or performance-critical in ways that benefit from hand-tuned SQL, the direct SQLite approach from the previous lesson remains entirely reasonable.

Assuming NSPersistentContainer's viewContext is ready to use immediately after alloc/initWithName:, without waiting for loadPersistentStores:'s completion handler.

Only fetch, insert, or save through viewContext after loadPersistentStores:'s completion handler has run and reported a nil error.

Creating the container does not open or create the store file — that happens asynchronously (or synchronously, but still only) inside loadPersistentStores:. Using the stack before that completes, or after it reports an error, is undefined and error-prone.

Writing NSPredicate predicateWithFormat:@"name == '%@'", someName with the value hand-quoted inside the format string.

Write NSPredicate predicateWithFormat:@"name == %@", someName and let the %@ substitution handle the value.

NSPredicate's format substitution (%@, %d, etc.) handles quoting and value substitution correctly and safely; hand-building the condition text yourself is unnecessary and error-prone, echoing the same string-concatenation pitfalls covered for raw SQL in the previous lesson.

Declaring a relationship in the data model with no inverse relationship set on the other entity.

Always declare both sides of a relationship and mark them as each other's inverse in the model editor.

Without an inverse, Core Data cannot automatically keep both sides of a relationship consistent when one side changes, and the framework will warn about missing inverses — they exist specifically to support that automatic bookkeeping.

Treating a fetch request's predicate and its sort descriptors as doing the same job, or expecting a predicate to control result order.

Use predicate to control which objects are included, and sortDescriptors (a separate, ordered array) to control the order results come back in.

A predicate filters; it has no effect on ordering. Order must be specified separately with one or more NSSortDescriptors, applied in the array's order to break ties.

What does NSPersistentContainer bundle together for you? A. Only the managed object model, nothing else B. The managed object model, the persistent store coordinator, and the store setup, exposing a ready-to-use viewContext C. A replacement for NSManagedObjectContext that you never need to create yourself D. A networked database server connection

B. The managed object model, the persistent store coordinator, and the store setup, exposing a ready-to-use viewContext — NSPersistentContainer assembles the model, the persistent store coordinator, and the store loading process, and exposes the result through a main-queue viewContext (plus newBackgroundContext for background work) — it's a convenience wrapper around the same layers, not a replacement for them.

What is a managed object context best described as? A. A cache of raw SQL query results B. A working scratchpad where fetched/created/modified objects live until explicitly saved C. A synonym for the persistent store coordinator D. A file format for storing the schema

B. A working scratchpad where fetched/created/modified objects live until explicitly saved — A managed object context holds your in-progress work — fetched, inserted, and modified objects — none of which reaches the persistent store until you explicitly call save: on that context.

In Core Data's terms, what is an entity most analogous to? A. A single row of data B. A SQL index C. A class — a named kind of object your app models, with attributes and relationships D. A predicate

C. A class — a named kind of object your app models, with attributes and relationships — An entity is Core Data's term for a modeled kind of object, analogous to a class; instances of that entity at runtime are managed objects with attribute values and relationships.

What is the purpose of declaring a relationship's inverse in the managed object model? A. It has no functional purpose, only documentation value B. It lets Core Data automatically keep both sides of the relationship consistent when one side changes C. It makes the relationship read-only D. It is required only for to-one relationships, never to-many

B. It lets Core Data automatically keep both sides of the relationship consistent when one side changes — With an inverse declared, setting one side of a relationship (e.g. a Task's project) automatically updates the other side (the Project's tasks) — Core Data uses the inverse to maintain that consistency for you.

What is the difference in role between a fetch request's predicate and its sort descriptors? A. They do the same thing and either can be used interchangeably B. The predicate controls ordering; sort descriptors control filtering C. The predicate controls which objects are included; sort descriptors control the order results come back in D. Sort descriptors can only be used without a predicate present

C. The predicate controls which objects are included; sort descriptors control the order results come back in — A predicate filters which objects match the fetch; sort descriptors (a separate, ordered array) determine the order of the results that do match — the two are independent and complementary.

By default, what underlying technology does Core Data's default persistent store use to actually store data on disk? A. A proprietary binary format unrelated to any other technology in this course B. A remote cloud database only C. SQLite — the same C library covered in the previous lesson D. Plain XML files exclusively, with no other option

C. SQLite — the same C library covered in the previous lesson — Core Data's default store type is backed by SQLite, meaning the object-graph operations you perform through fetch requests and saves are ultimately translated into SQL operations against a SQLite file, much like the SQL you wrote directly in the previous lesson.

Given a Core Data model with a Project entity (attribute: name) having a to-many relationship 'tasks' to a Task entity (attributes: title, isComplete; inverse relationship: project), write a method that fetches every Project whose name contains the substring "Website" (case-insensitive), sorted alphabetically by name, and returns the array.

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Project"]; request.predicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", @"Website"]; request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]; NSError *error = nil; NSArray *results = [context executeFetchRequest:request error:&error]; return results ?: @[];

Use NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", @"Website" for a case-and-diacritic-insensitive substring match, and a single NSSortDescriptor on name, ascending.