Core Data in Swift
Core Data is Apple’s powerful framework for persisting data in iOS applications. It provides object graph management and allows you to work with data as objects rather than SQL queries. In this guide, you’ll learn how to set up Core Data, create models, and perform CRUD operations.
What is Core Data?
Core Data is a framework for managing the model layer of your app. It provides:
- Object Graph Management - Work with objects instead of database rows
- Persistence - Save data permanently on disk
- Undo/Redo - Built-in undo/redo support
- Validation - Automatic data validation
- Relationships - Link objects together
- Memory Management - Efficient memory usage with faulting
Note: Core Data is NOT a database. It’s an object persistence framework that can use SQLite as its backing store.
Core Data Stack
The Core Data stack consists of:
1. NSManagedObjectModel - Describes your data schema2. NSPersistentStoreCoordinator - Manages the persistent store3. NSManagedObjectContext - Where you work with objects4. NSPersistentContainer - Wraps everything together (iOS 10+)Setting Up Core Data
Create with NSPersistentContainer (Modern Approach):
import CoreData
class PersistenceController { static let shared = PersistenceController()
let container: NSPersistentContainer
init() { container = NSPersistentContainer(name: "AppModel") container.loadPersistentStores { description, error in if let error = error { fatalError("Unable to load persistent stores: \(error)") } } }
var viewContext: NSManagedObjectContext { container.viewContext }}Creating Data Models
Define Entities in Xcode
- Create a
.xcdatamodeldfile - Add entities (like database tables)
- Add attributes (like columns)
- Define relationships
Example Entity: Person
Attributes:
name: Stringage: Int16email: String (optional)createdAt: Date
In Code (NSManagedObject Subclass):
import CoreData
@objc(Person)public class Person: NSManagedObject { @NSManaged public var name: String @NSManaged public var age: Int16 @NSManaged public var email: String? @NSManaged public var createdAt: Date}CRUD Operations
Create (Insert)
func createPerson(name: String, age: Int, email: String?) { let context = PersistenceController.shared.viewContext
let person = Person(context: context) person.name = name person.age = Int16(age) person.email = email person.createdAt = Date()
do { try context.save() print("✅ Person saved successfully") } catch { print("❌ Failed to save: \(error)") }}
// UsagecreatePerson(name: "John Doe", age: 30, email: "john@example.com")Read (Fetch)
Fetch All:
func fetchAllPeople() -> [Person] { let context = PersistenceController.shared.viewContext let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
do { let people = try context.fetch(fetchRequest) return people } catch { print("❌ Failed to fetch: \(error)") return [] }}
// Usagelet allPeople = fetchAllPeople()for person in allPeople { print("\(person.name), Age: \(person.age)")}Fetch with Predicate (Filter):
func fetchPeople(olderThan age: Int) -> [Person] { let context = PersistenceController.shared.viewContext let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
// Filter: age > specified age fetchRequest.predicate = NSPredicate(format: "age > %d", age)
// Sort by name fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
do { return try context.fetch(fetchRequest) } catch { print("❌ Failed to fetch: \(error)") return [] }}
// Usagelet adults = fetchPeople(olderThan: 18)Fetch with Sort:
func fetchPeopleSorted() -> [Person] { let context = PersistenceController.shared.viewContext let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: false), NSSortDescriptor(key: "name", ascending: true) ]
do { return try context.fetch(fetchRequest) } catch { return [] }}Update
func updatePerson(_ person: Person, newEmail: String) { let context = PersistenceController.shared.viewContext
person.email = newEmail
do { try context.save() print("✅ Person updated successfully") } catch { print("❌ Failed to update: \(error)") }}
// Usageif let firstPerson = fetchAllPeople().first { updatePerson(firstPerson, newEmail: "newemail@example.com")}Delete
func deletePerson(_ person: Person) { let context = PersistenceController.shared.viewContext
context.delete(person)
do { try context.save() print("✅ Person deleted successfully") } catch { print("❌ Failed to delete: \(error)") }}
// Delete all peoplefunc deleteAllPeople() { let context = PersistenceController.shared.viewContext let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Person.fetchRequest() let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do { try context.execute(deleteRequest) try context.save() print("✅ All people deleted") } catch { print("❌ Failed to delete: \(error)") }}Predicates (Filtering)
Basic Predicates
// Exact matchNSPredicate(format: "name == %@", "John")
// ContainsNSPredicate(format: "name CONTAINS[cd] %@", "john") // case/diacritic insensitive
// Starts withNSPredicate(format: "name BEGINSWITH %@", "J")
// Greater thanNSPredicate(format: "age > %d", 18)
// BetweenNSPredicate(format: "age BETWEEN {18, 65}")
// INNSPredicate(format: "name IN %@", ["John", "Jane", "Bob"])Compound Predicates
// ANDlet predicate1 = NSPredicate(format: "age > 18")let predicate2 = NSPredicate(format: "email != nil")let compound = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate1, predicate2])
// ORlet orPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [predicate1, predicate2])
// NOTlet notPredicate = NSCompoundPredicate(notPredicateWith: predicate1)Relationships
One-to-Many Relationship
Example: Person has many Posts
// Person entity@objc(Person)public class Person: NSManagedObject { @NSManaged public var name: String @NSManaged public var posts: NSSet?}
// Post entity@objc(Post)public class Post: NSManagedObject { @NSManaged public var title: String @NSManaged public var content: String @NSManaged public var author: Person?}
// Create person with postsfunc createPersonWithPosts() { let context = PersistenceController.shared.viewContext
let person = Person(context: context) person.name = "John"
let post1 = Post(context: context) post1.title = "First Post" post1.content = "Content here" post1.author = person
let post2 = Post(context: context) post2.title = "Second Post" post2.content = "More content" post2.author = person
do { try context.save() print("✅ Person with posts saved") } catch { print("❌ Error: \(error)") }}
// Access related objectsif let firstPerson = fetchAllPeople().first { if let posts = firstPerson.posts as? Set<Post> { for post in posts { print("Post: \(post.title)") } }}Many-to-Many Relationship
Example: Student enrolls in many Courses
@objc(Student)public class Student: NSManagedObject { @NSManaged public var name: String @NSManaged public var courses: NSSet?}
@objc(Course)public class Course: NSManagedObject { @NSManaged public var title: String @NSManaged public var students: NSSet?}
// Enroll student in coursefunc enrollStudent(_ student: Student, in course: Course) { let context = PersistenceController.shared.viewContext
student.addToCourses(course)
do { try context.save() } catch { print("Error: \(error)") }}Core Data with SwiftUI
Setup in App
import SwiftUI
@mainstruct MyApp: App { let persistenceController = PersistenceController.shared
var body: some Scene { WindowGroup { ContentView() .environment(\.managedObjectContext, persistenceController.viewContext) } }}Using @FetchRequest
import SwiftUIimport CoreData
struct PeopleListView: View { @Environment(\.managedObjectContext) private var viewContext
@FetchRequest( entity: Person.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Person.name, ascending: true)] ) private var people: FetchedResults<Person>
var body: some View { NavigationView { List { ForEach(people) { person in VStack(alignment: .leading) { Text(person.name) .font(.headline) Text("Age: \(person.age)") .font(.caption) } } .onDelete(perform: deletePeople) } .navigationTitle("People") .toolbar { Button("Add") { addPerson() } } } }
private func addPerson() { let newPerson = Person(context: viewContext) newPerson.name = "New Person" newPerson.age = 25 newPerson.createdAt = Date()
do { try viewContext.save() } catch { print("Error: \(error)") } }
private func deletePeople(offsets: IndexSet) { offsets.map { people[$0] }.forEach(viewContext.delete)
do { try viewContext.save() } catch { print("Error: \(error)") } }}Dynamic @FetchRequest
struct FilteredPeopleView: View { @Environment(\.managedObjectContext) private var viewContext @State private var minAge = 18
var body: some View { VStack { Stepper("Min Age: \(minAge)", value: $minAge, in: 0...100) .padding()
PeopleList(minAge: minAge) } }}
struct PeopleList: View { @FetchRequest private var people: FetchedResults<Person>
init(minAge: Int) { let predicate = NSPredicate(format: "age >= %d", minAge) _people = FetchRequest( entity: Person.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Person.name, ascending: true)], predicate: predicate ) }
var body: some View { List(people) { person in Text("\(person.name) - \(person.age)") } }}Complete CRUD Example with SwiftUI
import SwiftUIimport CoreData
struct PersonFormView: View { @Environment(\.managedObjectContext) private var viewContext @Environment(\.dismiss) private var dismiss
@State private var name = "" @State private var age = 18 @State private var email = ""
var body: some View { NavigationView { Form { Section("Personal Info") { TextField("Name", text: $name) Stepper("Age: \(age)", value: $age, in: 0...120) TextField("Email", text: $email) .keyboardType(.emailAddress) }
Section { Button("Save") { savePerson() } .disabled(name.isEmpty) } } .navigationTitle("Add Person") .toolbar { Button("Cancel") { dismiss() } } } }
private func savePerson() { let newPerson = Person(context: viewContext) newPerson.name = name newPerson.age = Int16(age) newPerson.email = email.isEmpty ? nil : email newPerson.createdAt = Date()
do { try viewContext.save() dismiss() } catch { print("Error saving: \(error)") } }}
struct PeopleView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest( entity: Person.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Person.name, ascending: true)] ) private var people: FetchedResults<Person>
@State private var showingAddPerson = false
var body: some View { NavigationView { List { ForEach(people) { person in NavigationLink { PersonDetailView(person: person) } label: { VStack(alignment: .leading) { Text(person.name) .font(.headline) HStack { Text("Age: \(person.age)") if let email = person.email { Text("• \(email)") } } .font(.caption) .foregroundColor(.gray) } } } .onDelete(perform: deletePeople) } .navigationTitle("People (\(people.count))") .toolbar { Button { showingAddPerson = true } label: { Image(systemName: "plus") } } .sheet(isPresented: $showingAddPerson) { PersonFormView() } } }
private func deletePeople(offsets: IndexSet) { offsets.map { people[$0] }.forEach(viewContext.delete)
do { try viewContext.save() } catch { print("Error deleting: \(error)") } }}Best Practices
1. Use Background Contexts for Heavy Operations
func importLargeDataset() { let container = PersistenceController.shared.container
container.performBackgroundTask { context in // Perform heavy work here for i in 0..<10000 { let person = Person(context: context) person.name = "Person \(i)" person.age = Int16(i % 100) }
do { try context.save() print("✅ Import complete") } catch { print("❌ Error: \(error)") } }}2. Save Efficiently
// ✅ Good - Save only when neededfunc saveBatch(_ items: [Data]) { let context = PersistenceController.shared.viewContext
for item in items { let person = Person(context: context) // Set properties }
// Save once after all inserts do { try context.save() } catch { print("Error: \(error)") }}
// ❌ Avoid - Saving in loopfor item in items { let person = Person(context: context) try? context.save() // Don't do this!}3. Handle Errors Properly
// ✅ Good - Proper error handlingdo { try viewContext.save()} catch let error as NSError { print("Core Data error: \(error), \(error.userInfo)")}
// ❌ Avoid - Silent failurestry? viewContext.save()4. Use Faulting for Memory Efficiency
// Core Data automatically faults (doesn't load all data)// Only load what you needfetchRequest.propertiesToFetch = ["name", "age"]fetchRequest.returnsObjectsAsFaults = trueSummary
Core Data provides powerful data persistence:
✅ Object-Oriented - Work with objects, not SQL
✅ Relationships - Link data naturally
✅ SwiftUI Integration - @FetchRequest for automatic updates
✅ Performance - Faulting and batching for efficiency
✅ Validation - Built-in data validation
Key Takeaways:
- Use NSPersistentContainer for setup
- Define entities in .xcdatamodeld file
- Perform CRUD through NSManagedObjectContext
- Use predicates for filtering
- @FetchRequest in SwiftUI for reactive data
- Save context after modifications
- Use background contexts for heavy operations
Congratulations! 🎉 You’ve completed the entire Swift learning guide! You now have the knowledge to build complete iOS applications with data persistence!