Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

Core Data is Apple’s powerful framework for managing object graphs and persisting data in iOS applications. Whether you’re building a simple note-taking app or a complex enterprise solution, mastering Core Data is essential for creating robust iOS applications that efficiently handle data storage and retrieval.

Core Data is Apple’s object graph and persistence framework that provides an interface between your objects and the underlying data store. Unlike simple data persistence solutions, Core Data offers:
Core Data isn’t just a database wrapper—it’s a complete data management solution that helps you build scalable iOS applications.
Understanding Core Data’s architecture is crucial for effective implementation. The framework consists of several key components:
import CoreData
class CoreDataStack {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data failed to load store: \(error)")
}
}
return container
}()
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
func save() {
if context.hasChanges {
do {
try context.save()
} catch {
print("Failed to save Core Data context: \(error)")
}
}
}
}
When creating a new iOS project in Xcode, simply check the “Use Core Data” checkbox. This automatically:
.xcdatamodeld fileIf you’re adding Core Data to an existing project, follow these steps:
import CoreData
// AppDelegate.swift or CoreDataManager.swift
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "TaskManager")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
Core Data uses entities to represent your data objects. Here’s how to create a robust data model:
.xcdatamodeld file// Example: Task entity
Entity Name: Task
Attributes:
- title: String
- content: String (Optional)
- createdDate: Date
- isCompleted: Bool (Default: NO)
- priority: Integer 16 (Default: 0)
Generate custom classes for your entities:
// Task+CoreDataClass.swift
import Foundation
import CoreData
@objc(Task)
public class Task: NSManagedObject {
convenience init(context: NSManagedObjectContext) {
self.init(entity: Task.entity(), insertInto: context)
self.createdDate = Date()
self.isCompleted = false
self.priority = 0
}
}
// Task+CoreDataProperties.swift
import Foundation
import CoreData
extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
return NSFetchRequest<Task>(entityName: "Task")
}
@NSManaged public var title: String?
@NSManaged public var content: String?
@NSManaged public var createdDate: Date?
@NSManaged public var isCompleted: Bool
@NSManaged public var priority: Int16
}
extension Task : Identifiable {
}
class TaskManager {
let context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
func createTask(title: String, content: String? = nil, priority: Int16 = 0) -> Task {
let task = Task(context: context)
task.title = title
task.content = content
task.priority = priority
saveContext()
return task
}
private func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch {
print("Save error: \(error)")
}
}
}
}
extension TaskManager {
func fetchAllTasks() -> [Task] {
let request: NSFetchRequest<Task> = Task.fetchRequest()
do {
return try context.fetch(request)
} catch {
print("Fetch error: \(error)")
return []
}
}
func fetchTasks(predicate: NSPredicate? = nil,
sortDescriptors: [NSSortDescriptor]? = nil,
limit: Int? = nil) -> [Task] {
let request: NSFetchRequest<Task> = Task.fetchRequest()
request.predicate = predicate
request.sortDescriptors = sortDescriptors
if let limit = limit {
request.fetchLimit = limit
}
do {
return try context.fetch(request)
} catch {
print("Fetch error: \(error)")
return []
}
}
func fetchCompletedTasks() -> [Task] {
let predicate = NSPredicate(format: "isCompleted == %@", NSNumber(value: true))
let sortDescriptor = NSSortDescriptor(key: "createdDate", ascending: false)
return fetchTasks(predicate: predicate, sortDescriptors: [sortDescriptor])
}
}
extension TaskManager {
func updateTask(_ task: Task, title: String? = nil, content: String? = nil, isCompleted: Bool? = nil) {
if let title = title {
task.title = title
}
if let content = content {
task.content = content
}
if let isCompleted = isCompleted {
task.isCompleted = isCompleted
}
saveContext()
}
func toggleTaskCompletion(_ task: Task) {
task.isCompleted.toggle()
saveContext()
}
}
extension TaskManager {
func deleteTask(_ task: Task) {
context.delete(task)
saveContext()
}
func deleteAllCompletedTasks() {
let completedTasks = fetchCompletedTasks()
for task in completedTasks {
context.delete(task)
}
saveContext()
}
func deleteTasks(matching predicate: NSPredicate) {
let request: NSFetchRequest<Task> = Task.fetchRequest()
request.predicate = predicate
do {
let tasks = try context.fetch(request)
for task in tasks {
context.delete(task)
}
saveContext()
} catch {
print("Delete error: \(error)")
}
}
}
Core Data excels at managing relationships between entities. Here’s how to implement one-to-many and many-to-many relationships:
// Category Entity
Entity: Category
Attributes:
- name: String
- colorHex: String
Relationships:
- tasks: To Many, Task.category, Delete Rule: Cascade
// Task Entity (updated)
Relationships:
- category: To One, Category.tasks, Delete Rule: Nullify
// Category+CoreDataClass.swift
@objc(Category)
public class Category: NSManagedObject {
convenience init(context: NSManagedObjectContext, name: String, colorHex: String = "#007AFF") {
self.init(entity: Category.entity(), insertInto: context)
self.name = name
self.colorHex = colorHex
}
var taskCount: Int {
return tasks?.count ?? 0
}
var completedTaskCount: Int {
guard let tasks = tasks as? Set<Task> else { return 0 }
return tasks.filter { $0.isCompleted }.count
}
}
// Enhanced TaskManager with Category support
extension TaskManager {
func createCategory(name: String, colorHex: String = "#007AFF") -> Category {
let category = Category(context: context, name: name, colorHex: colorHex)
saveContext()
return category
}
func createTask(title: String, content: String? = nil, category: Category? = nil) -> Task {
let task = Task(context: context)
task.title = title
task.content = content
task.category = category
saveContext()
return task
}
func fetchTasks(in category: Category) -> [Task] {
let predicate = NSPredicate(format: "category == %@", category)
let sortDescriptor = NSSortDescriptor(key: "createdDate", ascending: false)
return fetchTasks(predicate: predicate, sortDescriptors: [sortDescriptor])
}
}
extension TaskManager {
func fetchTasksWithAdvancedOptions() -> [Task] {
let request: NSFetchRequest<Task> = Task.fetchRequest()
// Complex predicate
let titlePredicate = NSPredicate(format: "title CONTAINS[cd] %@", "important")
let datePredicate = NSPredicate(format: "createdDate >= %@", Date().addingTimeInterval(-7*24*3600) as NSDate)
let completionPredicate = NSPredicate(format: "isCompleted == NO")
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
titlePredicate,
datePredicate,
completionPredicate
])
request.predicate = compoundPredicate
// Sorting
let prioritySort = NSSortDescriptor(key: "priority", ascending: false)
let dateSort = NSSortDescriptor(key: "createdDate", ascending: false)
request.sortDescriptors = [prioritySort, dateSort]
// Performance optimizations
request.fetchLimit = 50
request.fetchBatchSize = 10
do {
return try context.fetch(request)
} catch {
print("Advanced fetch error: \(error)")
return []
}
}
func performBatchOperation() {
// Batch update example
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "Task")
batchUpdateRequest.predicate = NSPredicate(format: "isCompleted == YES")
batchUpdateRequest.propertiesToUpdate = ["priority": 0]
batchUpdateRequest.resultType = .updatedObjectsCountResultType
do {
let result = try context.execute(batchUpdateRequest) as! NSBatchUpdateResult
print("Updated \(result.result!) tasks")
} catch {
print("Batch update failed: \(error)")
}
}
}
For table views and collection views, use NSFetchedResultsController for automatic updates:
import UIKit
import CoreData
class TaskListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var fetchedResultsController: NSFetchedResultsController<Task>!
var taskManager: TaskManager!
override func viewDidLoad() {
super.viewDidLoad()
setupFetchedResultsController()
do {
try fetchedResultsController.performFetch()
} catch {
print("Failed to fetch tasks: \(error)")
}
}
private func setupFetchedResultsController() {
let request: NSFetchRequest<Task> = Task.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "createdDate", ascending: false)
request.sortDescriptors = [sortDescriptor]
request.fetchBatchSize = 20
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: taskManager.context,
sectionNameKeyPath: nil,
cacheName: "TaskCache"
)
fetchedResultsController.delegate = self
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension TaskListViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.insertRows(at: [newIndexPath], with: .fade)
}
@unknown default:
tableView.reloadData()
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
Robust error handling is crucial for Core Data applications:
enum CoreDataError: Error {
case saveError(Error)
case fetchError(Error)
case validationError(String)
case migrationError(Error)
}
extension CoreDataError: LocalizedError {
var errorDescription: String? {
switch self {
case .saveError(let error):
return "Failed to save data: \(error.localizedDescription)"
case .fetchError(let error):
return "Failed to fetch data: \(error.localizedDescription)"
case .validationError(let message):
return "Data validation failed: \(message)"
case .migrationError(let error):
return "Data migration failed: \(error.localizedDescription)"
}
}
}
class SafeTaskManager: TaskManager {
override func saveContext() throws {
if context.hasChanges {
do {
try context.save()
} catch {
// Rollback changes on save failure
context.rollback()
throw CoreDataError.saveError(error)
}
}
}
func safeCreateTask(title: String, content: String? = nil) throws -> Task {
guard !title.isEmpty else {
throw CoreDataError.validationError("Title cannot be empty")
}
let task = Task(context: context)
task.title = title
task.content = content
try saveContext()
return task
}
func safeFetchTasks() throws -> [Task] {
let request: NSFetchRequest<Task> = Task.fetchRequest()
do {
return try context.fetch(request)
} catch {
throw CoreDataError.fetchError(error)
}
}
}
// Efficient batch processing
func processTasksInBatches() {
let request: NSFetchRequest<Task> = Task.fetchRequest()
request.fetchBatchSize = 50
do {
let tasks = try context.fetch(request)
for task in tasks {
// Process tasks - they're automatically faulted in as needed
processTask(task)
}
} catch {
print("Batch processing error: \(error)")
}
}
func performLargeDataOperation() {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = context
backgroundContext.perform {
// Perform heavy operations on background context
self.processLargeDataSet(in: backgroundContext)
// Save to parent context
do {
try backgroundContext.save()
// Save parent context on main queue
DispatchQueue.main.async {
self.saveContext()
}
} catch {
print("Background save error: \(error)")
}
}
}
// Good - uses index
let predicate1 = NSPredicate(format: "createdDate >= %@", startDate as NSDate)
// Good - specific comparison
let predicate2 = NSPredicate(format: "priority == %d", 1)
// Avoid - slow string operations
// let badPredicate = NSPredicate(format: "title LIKE '*important*'")
// Better - use CONTAINS with case-insensitive option
let goodPredicate = NSPredicate(format: "title CONTAINS[cd] %@", "important")
Problem: Accessing managed objects across different threads.
Solution: Use proper concurrency patterns:
// Correct way to pass data between contexts
func updateTaskOnBackground(_ taskObjectID: NSManagedObjectID) {
let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.perform {
do {
let task = try backgroundContext.existingObject(with: taskObjectID) as! Task
task.title = "Updated on background"
try backgroundContext.save()
} catch {
print("Background update error: \(error)")
}
}
}
Problem: Strong reference cycles with managed objects.
Solution: Use weak references appropriately:
class TaskViewController: UIViewController {
weak var task: Task? // Use weak reference
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
private func updateUI() {
guard let task = task else { return }
// Update UI with task data
}
}
Problem: Loading entire object graphs into memory.
Solution: Use faulting and batch processing:
func efficientDataProcessing() {
let request: NSFetchRequest<Task> = Task.fetchRequest()
request.returnsObjectsAsFaults = true // Default, but be explicit
request.fetchBatchSize = 20
// Only fetch specific properties if you don't need full objects
request.propertiesToFetch = ["title", "isCompleted"]
request.resultType = .dictionaryResultType
do {
let results = try context.fetch(request)
// Process dictionary results instead of full objects
} catch {
print("Efficient processing error: \(error)")
}
}
Core Data is a powerful framework that provides robust data persistence capabilities for iOS applications. By understanding its architecture, implementing proper CRUD operations, managing relationships effectively, and following performance best practices, you can build scalable applications that handle data efficiently.
Key takeaways for mastering Core Data:
With these fundamentals in place, you’re well-equipped to leverage Core Data’s full potential in your iOS applications. Whether you’re building a simple task manager or a complex data-driven application, Core Data provides the tools you need for robust data persistence.
This comprehensive Core Data tutorial covers everything from basic setup to advanced optimization techniques. Bookmark this guide and refer back to it as you develop your Core Data skills in iOS development.