Complete Core Data Guide: From Beginner to Advanced

 

Complete Core Data Guide: From Beginner to Advanced

A Comprehensive Tutorial for iOS Development with Swift


Table of Contents

  1. Introduction to Core Data
  2. Setting Up Core Data in Xcode
  3. Core Data Stack Components
  4. Data Models and Entities
  5. CRUD Operations
  6. Fetching Data
  7. Relationships
  8. Predicates and Sorting


1. Introduction to Core Data

What is Core Data?

Core Data is Apple's object graph and persistence framework for iOS, macOS, watchOS, and tvOS. Despite common misconceptions, Core Data is not a database but rather an object graph management framework that can persist data to disk using SQLite, XML, or binary formats.

Key Benefits

Core Data provides automatic memory management for object graphs, undo/redo functionality, lazy loading of data, built-in data validation, schema migration tools, and integration with iCloud. It reduces the amount of code needed to manage the model layer of applications by 50-70% compared to manual implementations.

When to Use Core Data

Use Core Data when you need to persist complex object graphs, manage relationships between entities, support undo/redo functionality, work with large datasets that benefit from lazy loading, or need built-in data validation and migration support. For simple key-value storage, UserDefaults or Keychain may be sufficient. For large amounts of unstructured data, consider using file-based storage.

Core Data vs Other Solutions

Compared to SQLite directly, Core Data offers higher-level abstractions and automatic memory management but may have more overhead for simple queries. Compared to Realm, Core Data is Apple's first-party solution with better system integration but has a steeper learning curve. Compared to SwiftData (iOS 17+), Core Data offers more control and works with older iOS versions.


2. Setting Up Core Data in Xcode

Creating a New Project with Core Data

When creating a new iOS project in Xcode, check the "Use Core Data" checkbox. This generates a Core Data stack in your AppDelegate or SceneDelegate, creates a .xcdatamodeld file for your data model, and sets up a basic persistent container.

Adding Core Data to Existing Project

To add Core Data to an existing project, create a new Data Model file (File > New > File > Core Data > Data Model). Import CoreData framework in files that need it. Create a Core Data stack (we'll cover this next). Add the Core Data model file to your target's Compile Sources.

Project Structure Best Practices

Organize Core Data files in a dedicated folder containing the .xcdatamodeld file, Core Data stack manager, Entity extensions, and Repository/Service classes. Keep Core Data implementation details separate from UI code using the Repository pattern or similar architectural approach.


3. Core Data Stack Components

Understanding the Stack

The Core Data stack consists of several key components that work together. The Managed Object Model (NSManagedObjectModel) represents your data model schema. The Persistent Store Coordinator (NSPersistentStoreCoordinator) coordinates between the object model and persistent stores. The Managed Object Context (NSManagedObjectContext) is where you work with your data objects. The Persistent Container (NSPersistentContainer) wraps these components (iOS 10+).

Creating a Core Data Stack (Modern Approach)

import CoreData


class CoreDataStack {

    static let shared = CoreDataStack()

    

    private init() {}

    

    // Persistent Container

    lazy var persistentContainer: NSPersistentContainer = {

        let container = NSPersistentContainer(name: "MyApp")

        

        container.loadPersistentStores { description, error in

            if let error = error {

                fatalError("Unable to load persistent stores: \(error)")

            }

        }

        

        // Configure container

        container.viewContext.automaticallyMergesChangesFromParent = true

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        

        return container

    }()

    

    // Main Context (UI Context)

    var mainContext: NSManagedObjectContext {

        return persistentContainer.viewContext

    }

    

    // Background Context for heavy operations

    func newBackgroundContext() -> NSManagedObjectContext {

        return persistentContainer.newBackgroundContext()

    }

    

    // Save Context

    func saveContext() {

        let context = mainContext

        if context.hasChanges {

            do {

                try context.save()

            } catch {

                let nserror = error as NSError

                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")

            }

        }

    }

}

Legacy Stack Implementation (Pre-iOS 10)

class LegacyCoreDataStack {

    static let shared = LegacyCoreDataStack()

    

    private init() {}

    

    lazy var managedObjectModel: NSManagedObjectModel = {

        guard let modelURL = Bundle.main.url(

            forResource: "MyApp", 

            withExtension: "momd"

        ) else {

            fatalError("Model not found")

        }

        

        guard let model = NSManagedObjectModel(contentsOf: modelURL) else {

            fatalError("Unable to load model")

        }

        

        return model

    }()

    

    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {

        let coordinator = NSPersistentStoreCoordinator(

            managedObjectModel: self.managedObjectModel

        )

        

        let urls = FileManager.default.urls(

            for: .documentDirectory, 

            in: .userDomainMask

        )

        let storeURL = urls.last!.appendingPathComponent("MyApp.sqlite")

        

        do {

            try coordinator.addPersistentStore(

                ofType: NSSQLiteStoreType,

                configurationName: nil,

                at: storeURL,

                options: [

                    NSMigratePersistentStoresAutomaticallyOption: true,

                    NSInferMappingModelAutomaticallyOption: true

                ]

            )

        } catch {

            fatalError("Error adding persistent store: \(error)")

        }

        

        return coordinator

    }()

    

    lazy var mainContext: NSManagedObjectContext = {

        let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

        context.persistentStoreCoordinator = self.persistentStoreCoordinator

        return context

    }()

}

Persistent Store Types

Core Data supports multiple store types. NSSQLiteStoreType is the default and recommended for most applications, offering good performance and compatibility. NSBinaryStoreType stores data as a binary file, loading the entire store into memory. NSInMemoryStoreType keeps data in memory only, useful for testing or temporary data. NSXMLStoreType (macOS only) stores data as XML.


4. Data Models and Entities

Creating Entities in Xcode

Open your .xcdatamodeld file in Xcode. Click the "Add Entity" button at the bottom. Name your entity using UpperCamelCase. Add attributes by clicking the "+" button under Attributes. Configure attribute properties including name, type, and optional settings.

Entity Configuration

// Example Entity: Person

// Attributes:

// - firstName: String

// - lastName: String

// - age: Integer 16

// - email: String (optional)

// - dateOfBirth: Date

// - isActive: Boolean (default: true)

Entity settings include the Module (Current Product Module for Swift), Class name, and Codegen options. Set Codegen to "Manual/None" if you want to write your own classes, "Class Definition" for automatic generation, or "Category/Extension" to add custom properties.

Attribute Types

Core Data supports multiple attribute types. Undefined is a placeholder that should be changed. Integer 16/32/64 for whole numbers of different sizes. Float and Double for decimal numbers. Decimal for precise decimal calculations (financial data). String for text data. Boolean for true/false values. Date for date and time. Binary Data for small binary objects. UUID for unique identifiers. URI for URLs. Transformable for custom types.

Transformable Attributes

For custom types not directly supported by Core Data, use Transformable attributes.

// Custom type

struct Address: Codable {

    let street: String

    let city: String

    let zipCode: String

}


// ValueTransformer

@objc(AddressTransformer)

class AddressTransformer: NSSecureUnarchiveFromDataTransformer {

    static let name = NSValueTransformerName(rawValue: "AddressTransformer")

    

    override static var allowedTopLevelClasses: [AnyClass] {

        return [Address.self, NSString.self]

    }

    

    static func register() {

        let transformer = AddressTransformer()

        ValueTransformer.setValueTransformer(transformer, forName: name)

    }

}


// Register in AppDelegate

AddressTransformer.register()


// In the data model, set:

// - Type: Transformable

// - Custom Class: Address

// - Transformer: AddressTransformer

Creating NSManagedObject Subclasses

Generate managed object subclasses by selecting your entity, then going to Editor > Create NSManagedObject Subclass. Choose your data model and entities. Select a location and create.

Example generated code:

import CoreData


@objc(Person)

public class Person: NSManagedObject {

    @NSManaged public var firstName: String?

    @NSManaged public var lastName: String?

    @NSManaged public var age: Int16

    @NSManaged public var email: String?

    @NSManaged public var dateOfBirth: Date?

    @NSManaged public var isActive: Bool

}


// Add custom properties and methods in extension

extension Person {

    var fullName: String {

        let first = firstName ?? ""

        let last = lastName ?? ""

        return "\(first) \(last)".trimmingCharacters(in: .whitespaces)

    }

    

    var isAdult: Bool {

        return age >= 18

    }

    

    func formattedBirthDate() -> String {

        guard let date = dateOfBirth else { return "Unknown" }

        let formatter = DateFormatter()

        formatter.dateStyle = .medium

        return formatter.string(from: date)

    }

}


5. CRUD Operations

Create (Insert)

Creating new objects in Core Data involves instantiating an NSManagedObject and setting its properties.

func createPerson(

    firstName: String,

    lastName: String,

    age: Int,

    email: String?

) -> Person? {

    let context = CoreDataStack.shared.mainContext

    

    let person = Person(context: context)

    person.firstName = firstName

    person.lastName = lastName

    person.age = Int16(age)

    person.email = email

    person.dateOfBirth = Date()

    person.isActive = true

    

    do {

        try context.save()

        print("Person created successfully")

        return person

    } catch {

        print("Failed to create person: \(error)")

        return nil

    }

}


// Usage

if let newPerson = createPerson(

    firstName: "John",

    lastName: "Doe",

    age: 30,

    email: "john@example.com"

) {

    print("Created: \(newPerson.fullName)")

}

Read (Fetch)

Fetching objects from Core Data uses NSFetchRequest.

func fetchAllPeople() -> [Person] {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    

    do {

        let people = try context.fetch(fetchRequest)

        return people

    } catch {

        print("Failed to fetch people: \(error)")

        return []

    }

}


// Fetch with predicate

func fetchPeopleByName(firstName: String) -> [Person] {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    fetchRequest.predicate = NSPredicate(format: "firstName == %@", firstName)

    

    do {

        let people = try context.fetch(fetchRequest)

        return people

    } catch {

        print("Failed to fetch people: \(error)")

        return []

    }

}


// Fetch single object

func fetchPersonById(objectID: NSManagedObjectID) -> Person? {

    let context = CoreDataStack.shared.mainContext

    

    do {

        let person = try context.existingObject(with: objectID) as? Person

        return person

    } catch {

        print("Failed to fetch person: \(error)")

        return nil

    }

}

Update

Updating objects involves fetching them, modifying properties, and saving the context.

func updatePerson(

    person: Person,

    firstName: String?,

    lastName: String?,

    age: Int?

) -> Bool {

    let context = CoreDataStack.shared.mainContext

    

    if let firstName = firstName {

        person.firstName = firstName

    }

    

    if let lastName = lastName {

        person.lastName = lastName

    }

    

    if let age = age {

        person.age = Int16(age)

    }

    

    do {

        try context.save()

        print("Person updated successfully")

        return true

    } catch {

        print("Failed to update person: \(error)")

        return false

    }

}


// Update by ID

func updatePersonByID(

    objectID: NSManagedObjectID,

    newEmail: String

) -> Bool {

    guard let person = fetchPersonById(objectID: objectID) else {

        return false

    }

    

    person.email = newEmail

    

    let context = CoreDataStack.shared.mainContext

    do {

        try context.save()

        return true

    } catch {

        print("Failed to update: \(error)")

        return false

    }

}

Delete

Deleting objects removes them from the context and persistent store.

func deletePerson(person: Person) -> Bool {

    let context = CoreDataStack.shared.mainContext

    context.delete(person)

    

    do {

        try context.save()

        print("Person deleted successfully")

        return true

    } catch {

        print("Failed to delete person: \(error)")

        return false

    }

}


// Delete multiple objects

func deleteAllPeople() -> Bool {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Person.fetchRequest()

    let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    

    do {

        try context.execute(batchDeleteRequest)

        try context.save()

        print("All people deleted")

        return true

    } catch {

        print("Failed to delete all people: \(error)")

        return false

    }

}


// Delete with predicate

func deletePeopleByAge(minAge: Int) -> Bool {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Person.fetchRequest()

    fetchRequest.predicate = NSPredicate(format: "age >= %d", minAge)

    

    let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    batchDeleteRequest.resultType = .resultTypeObjectIDs

    

    do {

        let result = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult

        let objectIDArray = result?.result as? [NSManagedObjectID] ?? []

        let changes = [NSDeletedObjectsKey: objectIDArray]

        NSManagedObjectContext.mergeChanges(

            fromRemoteContextSave: changes,

            into: [context]

        )

        return true

    } catch {

        print("Batch delete failed: \(error)")

        return false

    }

}


6. Fetching Data

Basic Fetch Requests

// Simple fetch

let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

let results = try? context.fetch(fetchRequest)



// Usage

func fetchPeople() -> Result<[Person], CoreDataError> {

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    

    do {

        let people = try context.fetch(fetchRequest)

        return .success(people)

    } catch {

        return .failure(.fetchFailed(error))

    }

}

Testing Core Data

class CoreDataTestStack {

    static let shared = CoreDataTestStack()

    

    lazy var persistentContainer: NSPersistentContainer = {

        let container = NSPersistentContainer(name: "MyApp")

        

        let description = NSPersistentStoreDescription()

        description.type = NSInMemoryStoreType

        container.persistentStoreDescriptions = [description]

        

        container.loadPersistentStores { description, error in

            if let error = error {

                fatalError("Failed to load test store: \(error)")

            }

        }

        

        return container

    }()

    

    var context: NS


// Fetch with limit

let limitedFetch: NSFetchRequest<Person> = Person.fetchRequest()

limitedFetch.fetchLimit = 10

let limitedResults = try? context.fetch(limitedFetch)


// Fetch with offset (pagination)

let paginatedFetch: NSFetchRequest<Person> = Person.fetchRequest()

paginatedFetch.fetchLimit = 20

paginatedFetch.fetchOffset = 40 // Skip first 40 results

let page3Results = try? context.fetch(paginatedFetch)

Fetch Request Builder Pattern

class PersonFetchRequestBuilder {

    private var fetchRequest: NSFetchRequest<Person>

    

    init() {

        fetchRequest = Person.fetchRequest()

    }

    

    func withPredicate(_ predicate: NSPredicate) -> PersonFetchRequestBuilder {

        fetchRequest.predicate = predicate

        return self

    }

    

    func sorted(by sortDescriptors: [NSSortDescriptor]) -> PersonFetchRequestBuilder {

        fetchRequest.sortDescriptors = sortDescriptors

        return self

    }

    

    func limit(_ limit: Int) -> PersonFetchRequestBuilder {

        fetchRequest.fetchLimit = limit

        return self

    }

    

    func offset(_ offset: Int) -> PersonFetchRequestBuilder {

        fetchRequest.fetchOffset = offset

        return self

    }

    

    func build() -> NSFetchRequest<Person> {

        return fetchRequest

    }

}


// Usage

let fetchRequest = PersonFetchRequestBuilder()

    .withPredicate(NSPredicate(format: "age > %d", 18))

    .sorted(by: [NSSortDescriptor(key: "lastName", ascending: true)])

    .limit(50)

    .build()

Fetched Results Controller

NSFetchedResultsController monitors Core Data changes and updates UI automatically, perfect for UITableView and UICollectionView.

class PeopleViewController: UIViewController {

    private var tableView: UITableView!

    private lazy var fetchedResultsController: NSFetchedResultsController<Person> = {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.sortDescriptors = [

            NSSortDescriptor(key: "lastName", ascending: true),

            NSSortDescriptor(key: "firstName", ascending: true)

        ]

        

        let controller = NSFetchedResultsController(

            fetchRequest: fetchRequest,

            managedObjectContext: CoreDataStack.shared.mainContext,

            sectionNameKeyPath: nil,

            cacheName: "PeopleCache"

        )

        

        controller.delegate = self

        

        return controller

    }()

    

    override func viewDidLoad() {

        super.viewDidLoad()

        setupTableView()

        performFetch()

    }

    

    private func performFetch() {

        do {

            try fetchedResultsController.performFetch()

            tableView.reloadData()

        } catch {

            print("Failed to fetch: \(error)")

        }

    }

}


// MARK: - UITableViewDataSource

extension PeopleViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {

        return fetchedResultsController.sections?.count ?? 0

    }

    

    func tableView(

        _ tableView: UITableView,

        numberOfRowsInSection section: Int

    ) -> Int {

        return fetchedResultsController.sections?[section].numberOfObjects ?? 0

    }

    

    func tableView(

        _ tableView: UITableView,

        cellForRowAt indexPath: IndexPath

    ) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(

            withIdentifier: "PersonCell",

            for: indexPath

        )

        

        let person = fetchedResultsController.object(at: indexPath)

        cell.textLabel?.text = person.fullName

        cell.detailTextLabel?.text = person.email

        

        return cell

    }

}


// MARK: - NSFetchedResultsControllerDelegate

extension PeopleViewController: 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: .automatic)

            }

        case .delete:

            if let indexPath = indexPath {

                tableView.deleteRows(at: [indexPath], with: .automatic)

            }

        case .update:

            if let indexPath = indexPath {

                tableView.reloadRows(at: [indexPath], with: .automatic)

            }

        case .move:

            if let indexPath = indexPath, let newIndexPath = newIndexPath {

                tableView.deleteRows(at: [indexPath], with: .automatic)

                tableView.insertRows(at: [newIndexPath], with: .automatic)

            }

        @unknown default:

            break

        }

    }

    

    func controllerDidChangeContent(

        _ controller: NSFetchedResultsController<NSFetchRequestResult>

    ) {

        tableView.endUpdates()

    }

}

Fetching Specific Properties

For better performance, fetch only the properties you need.

// Fetch specific attributes

func fetchPersonNames() -> [(firstName: String, lastName: String)] {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Person")

    fetchRequest.resultType = .dictionaryResultType

    fetchRequest.propertiesToFetch = ["firstName", "lastName"]

    

    do {

        let results = try context.fetch(fetchRequest)

        return results.compactMap { dict in

            guard let firstName = dict["firstName"] as? String,

                  let lastName = dict["lastName"] as? String else {

                return nil

            }

            return (firstName, lastName)

        }

    } catch {

        print("Failed to fetch names: \(error)")

        return []

    }

}


// Count without fetching objects

func countPeople() -> Int {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    

    do {

        let count = try context.count(for: fetchRequest)

        return count

    } catch {

        print("Failed to count: \(error)")

        return 0

    }

}


7. Relationships

Defining Relationships

Relationships connect entities. In the data model editor, select an entity, add a relationship, specify destination entity, set relationship type (to-one or to-many), configure inverse relationship (always recommended), and set delete rules.

Example data model with relationships:

Company

- name: String

- employees: To-Many -> Employee


Employee

- name: String

- employeeID: String

- company: To-One -> Company

- projects: To-Many -> Project


Project

- title: String

- deadline: Date

- employees: To-Many -> Employee

Delete Rules

No Action leaves related objects unchanged (can create inconsistencies). Nullify sets the inverse relationship to nil. Cascade deletes related objects. Deny prevents deletion if relationships exist.

Working with To-One Relationships

// Create company

let company = Company(context: context)

company.name = "Tech Corp"


// Create employee and assign to company

let employee = Employee(context: context)

employee.name = "Jane Smith"

employee.employeeID = "EMP001"

employee.company = company


try? context.save()


// Access relationship

if let employeeCompany = employee.company {

    print("Employee works at: \(employeeCompany.name ?? "Unknown")")

}

Working with To-Many Relationships

// Add employee to company

company.addToEmployees(employee)


// Add multiple employees

let employees = NSSet(array: [employee1, employee2, employee3])

company.addToEmployees(employees)


// Remove employee

company.removeFromEmployees(employee)


// Access all employees

if let employees = company.employees as? Set<Employee> {

    print("Company has \(employees.count) employees")

    for employee in employees {

        print("- \(employee.name ?? "Unknown")")

    }

}


// Many-to-many relationship

let project = Project(context: context)

project.title = "iOS App Redesign"

project.addToEmployees(employee)

employee.addToProjects(project)


try? context.save()

Fetching with Relationship Predicates

// Fetch employees from specific company

func fetchEmployees(forCompanyName name: String) -> [Employee] {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<Employee> = Employee.fetchRequest()

    fetchRequest.predicate = NSPredicate(format: "company.name == %@", name)

    

    return (try? context.fetch(fetchRequest)) ?? []

}


// Fetch companies with more than X employees

func fetchCompanies(withMinEmployees min: Int) -> [Company] {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<Company> = Company.fetchRequest()

    fetchRequest.predicate = NSPredicate(

        format: "employees.@count >= %d",

        min

    )

    

    return (try? context.fetch(fetchRequest)) ?? []

}


// Fetch employees working on specific project

func fetchEmployees(forProjectTitle title: String) -> [Employee] {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<Employee> = Employee.fetchRequest()

    fetchRequest.predicate = NSPredicate(

        format: "ANY projects.title == %@",

        title

    )

    

    return (try? context.fetch(fetchRequest)) ?? []

}

Faulting and Relationship Performance

Core Data uses faulting to improve memory usage. A fault is a placeholder for an object or relationship that hasn't been loaded yet.

// Prefetch relationships to avoid multiple round trips

let fetchRequest: NSFetchRequest<Company> = Company.fetchRequest()

fetchRequest.relationshipKeyPathsForPrefetching = ["employees", "employees.projects"]


let companies = try? context.fetch(fetchRequest)


// Now accessing employees won't trigger additional fetches

for company in companies ?? [] {

    if let employees = company.employees as? Set<Employee> {

        for employee in employees {

            // Already prefetched

            print(employee.projects?.count ?? 0)

        }

    }

}


8. Predicates and Sorting

Basic Predicates

// Exact match

let exactMatch = NSPredicate(format: "firstName == %@", "John")


// Case-insensitive comparison

let caseInsensitive = NSPredicate(format: "firstName ==[c] %@", "john")


// Not equal

let notEqual = NSPredicate(format: "firstName != %@", "John")


// Greater than, less than

let olderThan30 = NSPredicate(format: "age > %d", 30)

let youngerThan18 = NSPredicate(format: "age < %d", 18)

let between = NSPredicate(format: "age >= %d AND age <= %d", 18, 65)


// Contains

let containsString = NSPredicate(format: "firstName CONTAINS[c] %@", "oh")


// Begins with, ends with

let beginsWith = NSPredicate(format: "firstName BEGINSWITH[c] %@", "Jo")

let endsWith = NSPredicate(format: "lastName ENDSWITH[c] %@", "son")


// LIKE wildcard

let likePattern = NSPredicate(format: "email LIKE %@", "*@gmail.com")


// IN operator

let inList = NSPredicate(

    format: "firstName IN %@",

    ["John", "Jane", "Bob"]

)


// NULL checks

let hasEmail = NSPredicate(format: "email != nil")

let noEmail = NSPredicate(format: "email == nil")

Compound Predicates

// AND

let andPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [

    NSPredicate(format: "age > %d", 18),

    NSPredicate(format: "isActive == true")

])


// OR

let orPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [

    NSPredicate(format: "firstName == %@", "John"),

    NSPredicate(format: "firstName == %@", "Jane")

])


// NOT

let notPredicate = NSCompoundPredicate(notPredicateWithSubpredicate:

    NSPredicate(format: "age < %d", 18)

)


// Complex combination

let complexPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [

    NSPredicate(format: "age >= %d", 18),

    NSCompoundPredicate(orPredicateWithSubpredicates: [

        NSPredicate(format: "city == %@", "New York"),

        NSPredicate(format: "city == %@", "Los Angeles")

    ])

])

Date Predicates

// Today

let calendar = Calendar.current

let startOfDay = calendar.startOfDay(for: Date())

let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!


let todayPredicate = NSPredicate(

    format: "dateOfBirth >= %@ AND dateOfBirth < %@",

    startOfDay as NSDate,

    endOfDay as NSDate

)


// Last 7 days

let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: Date())!

let lastWeekPredicate = NSPredicate(

    format: "dateOfBirth >= %@",

    sevenDaysAgo as NSDate

)


// Between dates

func predicateBetween(start: Date, end: Date) -> NSPredicate {

    return NSPredicate(

        format: "dateOfBirth >= %@ AND dateOfBirth <= %@",

        start as NSDate,

        end as NSDate

    )

}

Collection Predicates

// ANY - at least one object in relationship matches

let anyPredicate = NSPredicate(

    format: "ANY projects.title CONTAINS[c] %@",

    "iOS"

)


// ALL - all objects in relationship match

let allPredicate = NSPredicate(

    format: "ALL employees.age >= %d",

    18

)


// NONE - no objects match

let nonePredicate = NSPredicate(

    format: "NONE projects.deadline < %@",

    Date() as NSDate

)


// Count

let countPredicate = NSPredicate(

    format: "employees.@count > %d",

    10

)


// Aggregation

let avgAgePredicate = NSPredicate(

    format: "employees.@avg.age > %f",

    30.0

)


let sumPredicate = NSPredicate(

    format: "projects.@sum.budget > %f",

    100000.0

)

Sort Descriptors

// Single sort

let sortByLastName = NSSortDescriptor(key: "lastName", ascending: true)


// Multiple sorts (priority order)

let sortDescriptors = [

    NSSortDescriptor(key: "lastName", ascending: true),

    NSSortDescriptor(key: "firstName", ascending: true)

]


// Case-insensitive sort

let caseInsensitiveSort = NSSortDescriptor(

    key: "firstName",

    ascending: true,

    selector: #selector(NSString.caseInsensitiveCompare(_:))

)


// Localized sort (respects language rules)

let localizedSort = NSSortDescriptor(

    key: "firstName",

    ascending: true,

    selector: #selector(NSString.localizedStandardCompare(_:))

)


// Sort by relationship property

let sortByCompanyName = NSSortDescriptor(

    key: "company.name",

    ascending: true

)


// Custom comparator

let customSort = NSSortDescriptor(

    key: "age",

    ascending: true,

    comparator: { obj1, obj2 in

        guard let age1 = obj1 as? Int16,

              let age2 = obj2 as? Int16 else {

            return .orderedSame

        }

        

        if age1 < age2 { return .orderedAscending }

        if age1 > age2 { return .orderedDescending }

        return .orderedSame

    }

)


// Using sort descriptors in fetch request

let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

fetchRequest.sortDescriptors = sortDescriptors

fetchRequest.predicate = NSPredicate(format: "age > %d", 18)


let results = try? context.fetch(fetchRequest)



9. Core Data Concurrency

Understanding Concurrency

Core Data is not thread-safe. Each NSManagedObjectContext is bound to a specific queue. The main context runs on the main queue (for UI updates). Background contexts run on private queues (for heavy operations). Never pass managed objects between contexts directly. Always use object IDs or context.perform methods.

Concurrency Types

Main Queue Concurrency Type (.mainQueueConcurrencyType) is used for UI-related operations and runs on the main thread. Private Queue Concurrency Type (.privateQueueConcurrencyType) is used for background operations and manages its own private queue. Confinement Queue Concurrency Type (legacy, deprecated) was the old approach before iOS 5.

Background Context Pattern

// Performing work in background

func importLargeDataset(jsonData: [[String: Any]]) {

    let backgroundContext = CoreDataStack.shared.newBackgroundContext()

    

    backgroundContext.perform {

        for data in jsonData {

            let person = Person(context: backgroundContext)

            person.firstName = data["firstName"] as? String

            person.lastName = data["lastName"] as? String

            person.age = Int16(data["age"] as? Int ?? 0)

        }

        

        do {

            try backgroundContext.save()

            print("Import completed")

        } catch {

            print("Failed to import: \(error)")

        }

    }

}


// Alternative with completion handler

func importData(

    completion: @escaping (Result<Void, Error>) -> Void

) {

    let backgroundContext = CoreDataStack.shared.newBackgroundContext()

    

    backgroundContext.perform {

        // Heavy work here

        do {

            try backgroundContext.save()

            

            DispatchQueue.main.async {

                completion(.success(()))

            }

        } catch {

            DispatchQueue.main.async {

                completion(.failure(error))

            }

        }

    }

}

Parent-Child Context Pattern

class CoreDataStack {

    // Parent context (private queue)

    lazy var privateManagedObjectContext: NSManagedObjectContext = {

        let context = NSManagedObjectContext(

            concurrencyType: .privateQueueConcurrencyType

        )

        context.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator

        return context

    }()

    

    // Child context (main queue)

    lazy var mainContext: NSManagedObjectContext = {

        let context = NSManagedObjectContext(

            concurrencyType: .mainQueueConcurrencyType

        )

        context.parent = privateManagedObjectContext

        return context

    }()

    

    // Save chain: child -> parent -> disk

    func saveContext() {

        let mainContext = self.mainContext

        let privateContext = self.privateManagedObjectContext

        

        mainContext.performAndWait {

            if mainContext.hasChanges {

                do {

                    try mainContext.save()

                } catch {

                    print("Main context save failed: \(error)")

                    return

                }

            }

        }

        

        privateContext.perform {

            if privateContext.hasChanges {

                do {

                    try privateContext.save()

                    print("Saved to disk")

                } catch {

                    print("Private context save failed: \(error)")

                }

            }

        }

    }

}

Passing Objects Between Contexts

// WRONG - Never do this

let backgroundContext = CoreDataStack.shared.newBackgroundContext()

backgroundContext.perform {

    // DON'T: person belongs to different context

    person.firstName = "New Name"

}


// CORRECT - Use object IDs

let objectID = person.objectID


let backgroundContext = CoreDataStack.shared.newBackgroundContext()

backgroundContext.perform {

    do {

        let personInBackground = try backgroundContext.existingObject(

            with: objectID

        ) as? Person

        personInBackground?.firstName = "New Name"

        try backgroundContext.save()

    } catch {

        print("Error: \(error)")

    }

}


// CORRECT - Use context.fetch with same predicate

func updatePersonInBackground(personID: String) {

    let backgroundContext = CoreDataStack.shared.newBackgroundContext()

    

    backgroundContext.perform {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.predicate = NSPredicate(

            format: "employeeID == %@",

            personID

        )

        

        do {

            if let person = try backgroundContext.fetch(fetchRequest).first {

                person.firstName = "Updated"

                try backgroundContext.save()

            }

        } catch {

            print("Error: \(error)")

        }

    }

}

Handling Context Changes

class DataManager {

    init() {

        setupContextNotifications()

    }

    

    private func setupContextNotifications() {

        NotificationCenter.default.addObserver(

            self,

            selector: #selector(contextDidSave(_:)),

            name: .NSManagedObjectContextDidSave,

            object: nil

        )

    }

    

    @objc private func contextDidSave(_ notification: Notification) {

        guard let context = notification.object as? NSManagedObjectContext else {

            return

        }

        

        // Don't merge changes from the main context into itself

        let mainContext = CoreDataStack.shared.mainContext

        if context != mainContext {

            mainContext.perform {

                mainContext.mergeChanges(fromContextDidSave: notification)

            }

        }

    }

}


// Automatic merging (iOS 10+)

let container = NSPersistentContainer(name: "MyApp")

container.viewContext.automaticallyMergesChangesFromParent = true

Batch Operations (Non-Context)

Batch operations bypass the context for better performance but don't update objects in memory.

// Batch update

func batchUpdateInactiveUsers() {

    let context = CoreDataStack.shared.mainContext

    let batchUpdate = NSBatchUpdateRequest(entityName: "Person")

    batchUpdate.predicate = NSPredicate(format: "lastLoginDate < %@", thirtyDaysAgo as NSDate)

    batchUpdate.propertiesToUpdate = ["isActive": false]

    batchUpdate.resultType = .updatedObjectIDsResultType

    

    do {

        let result = try context.execute(batchUpdate) as? NSBatchUpdateResult

        let objectIDArray = result?.result as? [NSManagedObjectID] ?? []

        let changes = [NSUpdatedObjectsKey: objectIDArray]

        NSManagedObjectContext.mergeChanges(

            fromRemoteContextSave: changes,

            into: [context]

        )

    } catch {

        print("Batch update failed: \(error)")

    }

}


// Batch delete (covered earlier)

func batchDeleteOldRecords() {

    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Person.fetchRequest()

    fetchRequest.predicate = NSPredicate(

        format: "createdDate < %@",

        oneYearAgo as NSDate

    )

    

    let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    batchDelete.resultType = .resultTypeObjectIDs

    

    // Execute and merge changes...

}


10. Migration and Versioning

When You Need Migration

You need migration when adding or removing entities or attributes, changing attribute types, modifying relationships, renaming entities or attributes, or changing entity hierarchies. Always create a new model version before making changes to production data.

Creating Model Versions

In Xcode, select your .xcdatamodeld file. Go to Editor > Add Model Version. Name the new version (usually v2, v3, etc.). Make your changes in the new version. Set the new version as current in the model inspector.

Lightweight Migration

Lightweight migration handles simple changes automatically, including adding or removing attributes (with default values), adding or removing entities, renaming attributes or entities (with renaming identifier), and changing optional to required (with default value).

// Enable lightweight migration

lazy var persistentContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: "MyApp")

    

    let description = container.persistentStoreDescriptions.first

    description?.shouldMigrateStoreAutomatically = true

    description?.shouldInferMappingModelAutomatically = true

    

    container.loadPersistentStores { description, error in

        if let error = error {

            fatalError("Unable to load persistent stores: \(error)")

        }

    }

    

    return container

}()


// Legacy approach

let options = [

    NSMigratePersistentStoresAutomaticallyOption: true,

    NSInferMappingModelAutomaticallyOption: true

]


try coordinator.addPersistentStore(

    ofType: NSSQLiteStoreType,

    configurationName: nil,

    at: storeURL,

    options: options

)

Renaming Attributes

Set renaming identifier in the model editor. Select the renamed attribute. In the inspector, set the Renaming ID to the old name. This tells Core Data that "newName" is actually "oldName" renamed.

Manual Migration

For complex changes, create a custom mapping model.

// Create mapping model

// File > New > File > Mapping Model

// Select source and destination models


// Custom migration policy

import CoreData


class PersonToEmployeeMigrationPolicy: NSEntityMigrationPolicy {

    override func createDestinationInstances(

        forSource sInstance: NSManagedObject,

        in mapping: NSEntityMapping,

        manager: NSMigrationManager

    ) throws {

        try super.createDestinationInstances(

            forSource: sInstance,

            in: mapping,

            manager: manager

        )

        

        guard let destObject = manager.destinationInstances(

            forEntityMappingName: mapping.name,

            sourceInstances: [sInstance]

        ).first else {

            return

        }

        

        // Custom migration logic

        if let fullName = sInstance.value(forKey: "fullName") as? String {

            let components = fullName.components(separatedBy: " ")

            destObject.setValue(components.first, forKey: "firstName")

            destObject.setValue(components.last, forKey: "lastName")

        }

    }

}


// Perform migration

func migrateStore(at storeURL: URL) {

    let sourceModel = NSManagedObjectModel(

        contentsOf: Bundle.main.url(

            forResource: "MyApp",

            withExtension: "momd/MyApp.mom"

        )!

    )!

    

    let destinationModel = NSManagedObjectModel(

        contentsOf: Bundle.main.url(

            forResource: "MyApp",

            withExtension: "momd/MyApp 2.mom"

        )!

    )!

    

    let mappingModel = NSMappingModel(

        from: [Bundle.main],

        forSourceModel: sourceModel,

        destinationModel: destinationModel

    )!

    

    let manager = NSMigrationManager(

        sourceModel: sourceModel,

        destinationModel: destinationModel

    )

    

    let destURL = storeURL.deletingLastPathComponent()

        .appendingPathComponent("MyApp_new.sqlite")

    

    try? manager.migrateStore(

        from: storeURL,

        sourceType: NSSQLiteStoreType,

        options: nil,

        with: mappingModel,

        toDestinationURL: destURL,

        destinationType: NSSQLiteStoreType,

        destinationOptions: nil

    )

    

    // Replace old store with new one

    try? FileManager.default.removeItem(at: storeURL)

    try? FileManager.default.moveItem(at: destURL, to: storeURL)

}

Progressive Migration

For apps with multiple versions, migrate progressively through each version.

func progressiveMigration(from sourceURL: URL, to destinationURL: URL) throws {

    let sourceMetadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(

        ofType: NSSQLiteStoreType,

        at: sourceURL,

        options: nil

    )

    

    let modelVersions = ["MyApp", "MyApp 2", "MyApp 3", "MyApp 4"]

    

    var currentURL = sourceURL

    

    for (index, version) in modelVersions.enumerated() {

        guard let model = NSManagedObjectModel(

            contentsOf: Bundle.main.url(

                forResource: version,

                withExtension: "mom",

                subdirectory: "MyApp.momd"

            )!

        ) else {

            continue

        }

        

        if model.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata) {

            // This is our source model

            continue

        }

        

        // Migrate to this version

        let nextURL = destinationURL.deletingLastPathComponent()

            .appendingPathComponent("temp_\(index).sqlite")

        

        try migrateStore(from: currentURL, to: nextURL, targetVersion: version)

        

        currentURL = nextURL

    }

    

    // Final move

    try FileManager.default.moveItem(at: currentURL, to: destinationURL)

}

Testing Migration

func testMigration() {

    // Create test store with old model

    let oldStoreURL = FileManager.default.temporaryDirectory

        .appendingPathComponent("test_old.sqlite")

    

    // Populate with test data

    let oldContainer = NSPersistentContainer(name: "MyApp")

    // ... add test data ...

    

    // Attempt migration

    let newContainer = NSPersistentContainer(name: "MyApp")

    let description = NSPersistentStoreDescription(url: oldStoreURL)

    description.shouldMigrateStoreAutomatically = true

    description.shouldInferMappingModelAutomatically = true

    newContainer.persistentStoreDescriptions = [description]

    

    newContainer.loadPersistentStores { description, error in

        if let error = error {

            XCTFail("Migration failed: \(error)")

        } else {

            print("Migration successful")

        }

    }

}


11. Performance Optimization

Fetch Request Optimization

// BAD: Fetching all objects

let allPeople = try? context.fetch(Person.fetchRequest())


// GOOD: Use batch sizes

let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

fetchRequest.fetchBatchSize = 20


// GOOD: Fetch only needed properties

fetchRequest.propertiesToFetch = ["firstName", "lastName"]

fetchRequest.resultType = .dictionaryResultType


// GOOD: Use faulting strategically

fetchRequest.returnsObjectsAsFaults = false // Load objects immediately

// OR

fetchRequest.returnsObjectsAsFaults = true // Load as faults (default)


// GOOD: Prefetch relationships

fetchRequest.relationshipKeyPathsForPrefetching = ["company", "projects"]

Batch Faulting

// Fetch with relationships efficiently

let fetchRequest: NSFetchRequest<Company> = Company.fetchRequest()

fetchRequest.relationshipKeyPathsForPrefetching = ["employees"]

fetchRequest.fetchBatchSize = 20


let companies = try? context.fetch(fetchRequest)


// Now accessing employees is efficient

companies?.forEach { company in

    // Already prefetched, no additional fetch

    print("\(company.name): \(company.employees?.count ?? 0) employees")

}

Using Faulting Strategically

// For large lists where details aren't immediately needed

fetchRequest.returnsObjectsAsFaults = true

let people = try? context.fetch(fetchRequest)


// Objects are loaded on demand

people?.forEach { person in

    // Properties accessed here trigger fault firing

    print(person.firstName)

}


// For immediate use of all data

fetchRequest.returnsObjectsAsFaults = false

let peopleFullyLoaded = try? context.fetch(fetchRequest)


// All data immediately available, no lazy loading

Memory Management

// Process large datasets in batches

func processLargeDataset() {

    let context = CoreDataStack.shared.newBackgroundContext()

    

    context.perform {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.fetchBatchSize = 100

        

        do {

            let people = try context.fetch(fetchRequest)

            

            for (index, person) in people.enumerated() {

                // Process person

                person.processData()

                

                // Reset context every 100 objects

                if index % 100 == 0 {

                    try context.save()

                    context.reset() // Clear memory

                }

            }

            

            // Final save

            try context.save()

        } catch {

            print("Error: \(error)")

        }

    }

}

Asynchronous Fetch Requests

func performAsyncFetch() {

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    

    let asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { result in

        guard let people = result.finalResult else { return }

        

        DispatchQueue.main.async {

            // Update UI with results

            self.updateUI(with: people)

        }

    }

    

    do {

        try context.execute(asyncFetchRequest)

    } catch {

        print("Async fetch failed: \(error)")

    }

}

Persistent History Tracking

For apps with multiple contexts or sync requirements, track changes over time.

// Enable persistent history

let description = container.persistentStoreDescriptions.first

description?.setOption(

    true as NSNumber,

    forKey: NSPersistentHistoryTrackingKey

)


// Fetch changes since last sync

func fetchChangesSinceLastSync(token: NSPersistentHistoryToken?) {

    let context = CoreDataStack.shared.newBackgroundContext()

    

    let request = NSPersistentHistoryChangeRequest.fetchHistory(after: token)

    

    do {

        let result = try context.execute(request) as? NSPersistentHistoryResult

        guard let transactions = result?.result as? [NSPersistentHistoryTransaction] else {

            return

        }

        

        for transaction in transactions {

            // Process changes

            processTransaction(transaction)

        }

        

        // Save last token

        if let lastToken = transactions.last?.token {

            saveToken(lastToken)

        }

    } catch {

        print("Failed to fetch history: \(error)")

    }

}

Indexing

Add indexes to frequently queried attributes.

// In data model editor:

// Select attribute > Data Model Inspector

// Check "Indexed" for frequently searched attributes


// Good candidates for indexing:

// - Primary keys or unique identifiers

// - Attributes used in WHERE clauses

// - Attributes used for sorting

// - Foreign keys in relationships


// Note: Indexes speed up reads but slow down writes

// Only index attributes that are queried frequently

Query Performance Tips

// Count without fetching

let count = try? context.count(for: fetchRequest)


// Fetch object IDs only

fetchRequest.resultType = .managedObjectIDResultType

let objectIDs = try? context.fetch(fetchRequest) as? [NSManagedObjectID]


// Use batch limits for pagination

fetchRequest.fetchLimit = 50

fetchRequest.fetchOffset = currentPage * 50


// Avoid expensive predicates

// BAD: Regex or complex string operations

NSPredicate(format: "name MATCHES %@", ".*John.*")


// GOOD: Simple comparisons

NSPredicate(format: "name CONTAINS[c] %@", "John")


12. Advanced Patterns

Repository Pattern

Separate Core Data logic from the rest of your app.

protocol PersonRepository {

    func create(firstName: String, lastName: String, age: Int) -> Person?

    func fetch(predicate: NSPredicate?) -> [Person]

    func update(person: Person, changes: [String: Any]) -> Bool

    func delete(person: Person) -> Bool

}


class CoreDataPersonRepository: PersonRepository {

    private let context: NSManagedObjectContext

    

    init(context: NSManagedObjectContext = CoreDataStack.shared.mainContext) {

        self.context = context

    }

    

    func create(firstName: String, lastName: String, age: Int) -> Person? {

        let person = Person(context: context)

        person.firstName = firstName

        person.lastName = lastName

        person.age = Int16(age)

        

        do {

            try context.save()

            return person

        } catch {

            print("Failed to create: \(error)")

            return nil

        }

    }

    

    func fetch(predicate: NSPredicate? = nil) -> [Person] {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.predicate = predicate

        

        return (try? context.fetch(fetchRequest)) ?? []

    }

    

    func update(person: Person, changes: [String: Any]) -> Bool {

        for (key, value) in changes {

            person.setValue(value, forKey: key)

        }

        

        do {

            try context.save()

            return true

        } catch {

            print("Failed to update: \(error)")

            return false

        }

    }

    

    func delete(person: Person) -> Bool {

        context.delete(person)

        

        do {

            try context.save()

            return true

        } catch {

            print("Failed to delete: \(error)")

            return false

        }

    }

}


// Usage in ViewModel or Controller

class PeopleViewModel {

    private let repository: PersonRepository

    

    init(repository: PersonRepository = CoreDataPersonRepository()) {

        self.repository = repository

    }

    

    func loadPeople() -> [Person] {

        return repository.fetch(predicate: nil)

    }

    

    func addPerson(firstName: String, lastName: String, age: Int) {

        _ = repository.create(firstName: firstName, lastName: lastName, age: age)

    }

}

Unit of Work Pattern

Group related changes together.

class UnitOfWork {

    private let context: NSManagedObjectContext

    private var operations: [() -> Void] = []

    

    init(context: NSManagedObjectContext) {

        self.context = context

    }

    

    func register(operation: @escaping () -> Void) {

        operations.append(operation)

    }

    

    func commit() -> Result<Void, Error> {

        operations.forEach { $0() }

        

        do {

            try context.save()

            operations.removeAll()

            return .success(())

        } catch {

            context.rollback()

            return .failure(error)

        }

    }

    

    func rollback() {

        context.rollback()

        operations.removeAll()

    }

}


// Usage

let unitOfWork = UnitOfWork(context: context)


unitOfWork.register {

    let person = Person(context: self.context)

    person.firstName = "John"

}


unitOfWork.register {

    let company = Company(context: self.context)

    company.name = "Tech Corp"

}


switch unitOfWork.commit() {

case .success:

    print("All changes saved")

case .failure(let error):

    print("Failed: \(error)")

}

Generic Core Data Manager

protocol ManagedObjectConvertible {

    associatedtype ManagedObject: NSManagedObject

    

    func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject

}


class CoreDataManager<T: NSManagedObject> {

    private let context: NSManagedObjectContext

    

    init(context: NSManagedObjectContext = CoreDataStack.shared.mainContext) {

        self.context = context

    }

    

    func fetch(

        predicate: NSPredicate? = nil,

        sortDescriptors: [NSSortDescriptor]? = nil,

        fetchLimit: Int? = nil

    ) -> [T] {

        let fetchRequest = NSFetchRequest<T>(entityName: String(describing: T.self))

        fetchRequest.predicate = predicate

        fetchRequest.sortDescriptors = sortDescriptors

        if let limit = fetchLimit {

            fetchRequest.fetchLimit = limit

        }

        

        return (try? context.fetch(fetchRequest)) ?? []

    }

    

    func create() -> T {

        return T(context: context)

    }

    

    func save() -> Bool {

        guard context.hasChanges else { return true }

        

        do {

            try context.save()

            return true

        } catch {

            print("Save failed: \(error)")

            return false

        }

    }

    

    func delete(_ object: T) -> Bool {

        context.delete(object)

        return save()

    }

    

    func deleteAll() -> Bool {

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(

            entityName: String(describing: T.self)

        )

        let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)

        

        do {

            try context.execute(batchDelete)

            return true

        } catch {

            print("Batch delete failed: \(error)")

            return false

        }

    }

}


// Usage

let personManager = CoreDataManager<Person>()

let people = personManager.fetch(

    predicate: NSPredicate(format: "age > %d", 18),

    sortDescriptors: [NSSortDescriptor(key: "lastName", ascending: true)]

)


let newPerson = personManager.create()

newPerson.firstName = "Alice"

personManager.save()

Sync Manager Pattern

For syncing with remote servers.

class SyncManager {

    private let context: NSManagedObjectContext

    private let apiClient: APIClient

    

    init() {

        self.context = CoreDataStack.shared.newBackgroundContext()

        self.apiClient = APIClient()

    }

    

    func syncPeople() async throws {

        // Fetch from server

        let remotePeople = try await apiClient.fetchPeople()

        

        // Update local database

        context.perform {

            for remoteData in remotePeople {

                let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

                fetchRequest.predicate = NSPredicate(

                    format: "remoteID == %@",

                    remoteData.id

                )

                

                let person: Person

                if let existing = try? self.context.fetch(fetchRequest).first {

                    person = existing

                } else {

                    person = Person(context: self.context)

                    person.remoteID = remoteData.id

                }

                

                person.firstName = remoteData.firstName

                person.lastName = remoteData.lastName

                person.age = Int16(remoteData.age)

                person.lastSyncDate = Date()

            }

            

            do {

                try self.context.save()

            } catch {

                print("Sync failed: \(error)")

            }

        }

    }

    

    func pushLocalChanges() async throws {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.predicate = NSPredicate(

            format: "lastModifiedDate > lastSyncDate OR lastSyncDate == nil"

        )

        

        let changedPeople = try context.fetch(fetchRequest)

        

        for person in changedPeople {

            let data = PersonDTO(from: person)

            try await apiClient.updatePerson(data)

            person.lastSyncDate = Date()

        }

        

        try context.save()

    }

}


13. Best Practices

Naming Conventions

Use descriptive entity names in UpperCamelCase. Use clear attribute names in lowerCamelCase. Avoid abbreviations unless universally understood. Use relationship names that describe the connection. Add inverse relationships for all relationships. Use plural names for to-many relationships.

Data Validation

extension Person {

    override func awakeFromInsert() {

        super.awakeFromInsert()

        // Set default values

        self.isActive = true

        self.createdDate = Date()

    }

    

    override func validateForInsert() throws {

        try super.validateForInsert()

        try validatePerson()

    }

    

    override func validateForUpdate() throws {

        try super.validateForUpdate()

        try validatePerson()

    }

    

    private func validatePerson() throws {

        if let firstName = firstName, firstName.trimmingCharacters(in: .whitespaces).isEmpty {

            throw NSError(

                domain: "PersonValidation",

                code: 1,

                userInfo: [NSLocalizedDescriptionKey: "First name cannot be empty"]

            )

        }

        

        if age < 0 || age > 150 {

            throw NSError(

                domain: "PersonValidation",

                code: 2,

                userInfo: [NSLocalizedDescriptionKey: "Invalid age"]

            )

        }

        

        if let email = email, !email.contains("@") {

            throw NSError(

                domain: "PersonValidation",

                code: 3,

                userInfo: [NSLocalizedDescriptionKey: "Invalid email format"]

            )

        }

    }

}


// Handle validation errors

do {

    try context.save()

} catch let error as NSError {

    if error.domain == "PersonValidation" {

        // Handle validation error

        print("Validation failed: \(error.localizedDescription)")

    }

}

Error Handling

enum CoreDataError: Error {

    case fetchFailed(Error)

    case saveFailed(Error)

    case deleteFailed(Error)

    case invalidObject

    case contextNotAvailable

    

    var localizedDescription: String {

        switch self {

        case .fetchFailed(let error):

            return "Failed to fetch data: \(error.localizedDescription)"

        case .saveFailed(let error):

            return "Failed to save data: \(error.localizedDescription)"

        case .deleteFailed(let error):

            return "Failed to delete data: \(error.localizedDescription)"

        case .invalidObject:

            return "The object is invalid"

        case .contextNotAvailable:

            return "Core Data context is not available"

        }

    }

}



Table of Contents

  1. Core Data Concurrency
  2. Migration and Versioning
  3. Performance Optimization
  4. Advanced Patterns
  5. Best Practices
  6. Troubleshooting


1. Core Data Concurrency

Understanding Concurrency

Core Data is not thread-safe. Each NSManagedObjectContext is bound to a specific queue. The main context runs on the main queue (for UI updates). Background contexts run on private queues (for heavy operations). Never pass managed objects between contexts directly. Always use object IDs or context.perform methods.

Concurrency Types

Main Queue Concurrency Type (.mainQueueConcurrencyType) is used for UI-related operations and runs on the main thread.

Private Queue Concurrency Type (.privateQueueConcurrencyType) is used for background operations and manages its own private queue.

Confinement Queue Concurrency Type (legacy, deprecated) was the old approach before iOS 5.

Background Context Pattern

// Performing work in background

func importLargeDataset(jsonData: [[String: Any]]) {

    let backgroundContext = CoreDataStack.shared.newBackgroundContext()

    

    backgroundContext.perform {

        for data in jsonData {

            let person = Person(context: backgroundContext)

            person.firstName = data["firstName"] as? String

            person.lastName = data["lastName"] as? String

            person.age = Int16(data["age"] as? Int ?? 0)

        }

        

        do {

            try backgroundContext.save()

            print("Import completed")

        } catch {

            print("Failed to import: \(error)")

        }

    }

}


// Alternative with completion handler

func importData(completion: @escaping (Result<Void, Error>) -> Void) {

    let backgroundContext = CoreDataStack.shared.newBackgroundContext()

    

    backgroundContext.perform {

        // Heavy work here

        do {

            try backgroundContext.save()

            

            DispatchQueue.main.async {

                completion(.success(()))

            }

        } catch {

            DispatchQueue.main.async {

                completion(.failure(error))

            }

        }

    }

}

Parent-Child Context Pattern

class CoreDataStack {

    // Parent context (private queue)

    lazy var privateManagedObjectContext: NSManagedObjectContext = {

        let context = NSManagedObjectContext(

            concurrencyType: .privateQueueConcurrencyType

        )

        context.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator

        return context

    }()

    

    // Child context (main queue)

    lazy var mainContext: NSManagedObjectContext = {

        let context = NSManagedObjectContext(

            concurrencyType: .mainQueueConcurrencyType

        )

        context.parent = privateManagedObjectContext

        return context

    }()

    

    // Save chain: child -> parent -> disk

    func saveContext() {

        let mainContext = self.mainContext

        let privateContext = self.privateManagedObjectContext

        

        mainContext.performAndWait {

            if mainContext.hasChanges {

                do {

                    try mainContext.save()

                } catch {

                    print("Main context save failed: \(error)")

                    return

                }

            }

        }

        

        privateContext.perform {

            if privateContext.hasChanges {

                do {

                    try privateContext.save()

                    print("Saved to disk")

                } catch {

                    print("Private context save failed: \(error)")

                }

            }

        }

    }

}

Passing Objects Between Contexts

// WRONG - Never do this

let backgroundContext = CoreDataStack.shared.newBackgroundContext()

backgroundContext.perform {

    // DON'T: person belongs to different context

    person.firstName = "New Name"

}


// CORRECT - Use object IDs

let objectID = person.objectID


let backgroundContext = CoreDataStack.shared.newBackgroundContext()

backgroundContext.perform {

    do {

        let personInBackground = try backgroundContext.existingObject(

            with: objectID

        ) as? Person

        personInBackground?.firstName = "New Name"

        try backgroundContext.save()

    } catch {

        print("Error: \(error)")

    }

}


// CORRECT - Use context.fetch with same predicate

func updatePersonInBackground(personID: String) {

    let backgroundContext = CoreDataStack.shared.newBackgroundContext()

    

    backgroundContext.perform {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.predicate = NSPredicate(format: "employeeID == %@", personID)

        

        do {

            if let person = try backgroundContext.fetch(fetchRequest).first {

                person.firstName = "Updated"

                try backgroundContext.save()

            }

        } catch {

            print("Error: \(error)")

        }

    }

}

Handling Context Changes

class DataManager {

    init(context: NSManagedObjectContext = CoreDataStack.shared.mainContext) {

        self.context = context

    }

    

    func getAll() async throws -> [Person] {

        return try await context.perform {

            let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

            return try self.context.fetch(fetchRequest)

        }

    }

    

    func getById(_ id: NSManagedObjectID) async throws -> Person? {

        return try await context.perform {

            try self.context.existingObject(with: id) as? Person

        }

    }

    

    func create(_ object: Person) async throws {

        try await context.perform {

            try self.context.save()

        }

    }

    

    func update(_ object: Person) async throws {

        try await context.perform {

            if self.context.hasChanges {

                try self.context.save()

            }

        }

    }

    

    func delete(_ object: Person) async throws {

        try await context.perform {

            self.context.delete(object)

            try self.context.save()

        }

    }

}


5. Best Practices

Naming Conventions

  • Use descriptive entity names in UpperCamelCase
  • Use clear attribute names in lowerCamelCase
  • Avoid abbreviations unless universally understood
  • Use relationship names that describe the connection
  • Add inverse relationships for all relationships
  • Use plural names for to-many relationships

Data Validation

extension Person {

    override func awakeFromInsert() {

        super.awakeFromInsert()

        // Set default values

        self.isActive = true

        self.createdDate = Date()

    }

    

    override func validateForInsert() throws {

        try super.validateForInsert()

        try validatePerson()

    }

    

    override func validateForUpdate() throws {

        try super.validateForUpdate()

        try validatePerson()

    }

    

    private func validatePerson() throws {

        if let firstName = firstName, 

           firstName.trimmingCharacters(in: .whitespaces).isEmpty {

            throw NSError(

                domain: "PersonValidation",

                code: 1,

                userInfo: [NSLocalizedDescriptionKey: "First name cannot be empty"]

            )

        }

        

        if age < 0 || age > 150 {

            throw NSError(

                domain: "PersonValidation",

                code: 2,

                userInfo: [NSLocalizedDescriptionKey: "Invalid age"]

            )

        }

        

        if let email = email, !email.contains("@") {

            throw NSError(

                domain: "PersonValidation",

                code: 3,

                userInfo: [NSLocalizedDescriptionKey: "Invalid email format"]

            )

        }

    }

}


// Handle validation errors

do {

    try context.save()

} catch let error as NSError {

    if error.domain == "PersonValidation" {

        // Handle validation error

        print("Validation failed: \(error.localizedDescription)")

    }

}

Error Handling

enum CoreDataError: Error {

    case fetchFailed(Error)

    case saveFailed(Error)

    case deleteFailed(Error)

    case invalidObject

    case contextNotAvailable

    

    var localizedDescription: String {

        switch self {

        case .fetchFailed(let error):

            return "Failed to fetch data: \(error.localizedDescription)"

        case .saveFailed(let error):

            return "Failed to save data: \(error.localizedDescription)"

        case .deleteFailed(let error):

            return "Failed to delete data: \(error.localizedDescription)"

        case .invalidObject:

            return "The object is invalid"

        case .contextNotAvailable:

            return "Core Data context is not available"

        }

    }

}


// Usage

func fetchPeople() -> Result<[Person], CoreDataError> {

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    

    do {

        let people = try context.fetch(fetchRequest)

        return .success(people)

    } catch {

        return .failure(.fetchFailed(error))

    }

}


// With async/await

func fetchPeopleAsync() async throws -> [Person] {

    return try await context.perform {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        return try self.context.fetch(fetchRequest)

    }

}

Testing Core Data

class CoreDataTestStack {

    static let shared = CoreDataTestStack()

    

    lazy var persistentContainer: NSPersistentContainer = {

        let container = NSPersistentContainer(name: "MyApp")

        

        let description = NSPersistentStoreDescription()

        description.type = NSInMemoryStoreType

        container.persistentStoreDescriptions = [description]

        

        container.loadPersistentStores { description, error in

            if let error = error {

                fatalError("Failed to load test store: \(error)")

            }

        }

        

        return container

    }()

    

    var context: NSManagedObjectContext {

        return persistentContainer.viewContext

    }

    

    func reset() {

        let entities = persistentContainer.managedObjectModel.entities

        for entity in entities {

            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity.name!)

            let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)

            try? context.execute(batchDelete)

        }

    }

}


// XCTest example

import XCTest


class PersonRepositoryTests: XCTestCase {

    var repository: CoreDataPersonRepository!

    var context: NSManagedObjectContext!

    

    override func setUp() {

        super.setUp()

        context = CoreDataTestStack.shared.context

        repository = CoreDataPersonRepository(context: context)

        CoreDataTestStack.shared.reset()

    }

    

    override func tearDown() {

        repository = nil

        context = nil

        super.tearDown()

    }

    

    func testCreatePerson() {

        // Given

        let firstName = "John"

        let lastName = "Doe"

        let age = 30

        

        // When

        let person = repository.create(

            firstName: firstName,

            lastName: lastName,

            age: age

        )

        

        // Then

        XCTAssertNotNil(person)

        XCTAssertEqual(person?.firstName, firstName)

        XCTAssertEqual(person?.lastName, lastName)

        XCTAssertEqual(person?.age, Int16(age))

    }

    

    func testFetchPeople() {

        // Given

        _ = repository.create(firstName: "John", lastName: "Doe", age: 30)

        _ = repository.create(firstName: "Jane", lastName: "Smith", age: 25)

        

        // When

        let people = repository.fetch(predicate: nil)

        

        // Then

        XCTAssertEqual(people.count, 2)

    }

    

    func testUpdatePerson() {

        // Given

        guard let person = repository.create(

            firstName: "John", 

            lastName: "Doe", 

            age: 30

        ) else {

            XCTFail("Failed to create person")

            return

        }

        

        // When

        let success = repository.update(person, changes: ["firstName": "Johnny"])

        

        // Then

        XCTAssertTrue(success)

        XCTAssertEqual(person.firstName, "Johnny")

    }

    

    func testDeletePerson() {

        // Given

        guard let person = repository.create(

            firstName: "John", 

            lastName: "Doe", 

            age: 30

        ) else {

            XCTFail("Failed to create person")

            return

        }

        

        // When

        let success = repository.delete(person: person)

        

        // Then

        XCTAssertTrue(success)

        XCTAssertEqual(repository.fetch(predicate: nil).count, 0)

    }

}

Memory Management Best Practices

// Always reset contexts after processing large batches

func processLargeImport() {

    let context = CoreDataStack.shared.newBackgroundContext()

    

    context.perform {

        autoreleasepool {

            for batch in dataBatches {

                for item in batch {

                    let person = Person(context: context)

                    // Set properties

                }

                

                try? context.save()

                context.reset() // Clear memory

            }

        }

    }

}


// Use weak references for managed objects

class PersonViewModel {

    weak var person: Person? // Use weak to avoid retain cycles

    

    init(person: Person) {

        self.person = person

    }

}


// Disable undo manager if not needed

context.undoManager = nil

Security Considerations

// Enable file protection for sensitive data

let description = container.persistentStoreDescriptions.first

description?.setOption(

    FileProtectionType.complete as NSObject,

    forKey: NSPersistentStoreFileProtectionKey

)


// Validate input before saving

func createSecurePerson(firstName: String, lastName: String) -> Person? {

    // Sanitize input

    let cleanFirstName = firstName.trimmingCharacters(in: .whitespacesAndNewlines)

    let cleanLastName = lastName.trimmingCharacters(in: .whitespacesAndNewlines)

    

    guard !cleanFirstName.isEmpty && !cleanLastName.isEmpty else {

        return nil

    }

    

    // Additional validation

    guard cleanFirstName.count <= 100 && cleanLastName.count <= 100 else {

        return nil

    }

    

    let person = Person(context: context)

    person.firstName = cleanFirstName

    person.lastName = cleanLastName

    

    try? context.save()

    return person

}


// Encrypt sensitive attributes using Transformable

class EncryptedStringTransformer: ValueTransformer {

    override func transformedValue(_ value: Any?) -> Any? {

        guard let string = value as? String else { return nil }

        // Implement encryption

        return encrypt(string)

    }

    

    override func reverseTransformedValue(_ value: Any?) -> Any? {

        guard let data = value as? Data else { return nil }

        // Implement decryption

        return decrypt(data)

    }

}

Coding Guidelines

// 1. Always check context.hasChanges before saving

if context.hasChanges {

    try context.save()

}


// 2. Use meaningful entity and attribute names

// Good: Person, firstName, dateOfBirth

// Bad: P, fName, dob


// 3. Always set inverse relationships

// In Company: employees (to-many) -> Employee

// In Employee: company (to-one) -> Company


// 4. Use appropriate delete rules

// Cascade: Delete related objects

// Nullify: Set relationship to nil

// Deny: Prevent deletion if relationships exist


// 5. Implement proper error handling

do {

    try context.save()

} catch let error as NSError {

    print("Save failed: \(error), \(error.userInfo)")

}


// 6. Use background contexts for heavy operations

let backgroundContext = container.newBackgroundContext()

backgroundContext.perform {

    // Heavy work here

}


// 7. Prefetch relationships to avoid N+1 queries

fetchRequest.relationshipKeyPathsForPrefetching = ["company", "projects"]


// 8. Use batch operations for bulk updates/deletes

let batchUpdate = NSBatchUpdateRequest(entityName: "Person")

batchUpdate.propertiesToUpdate = ["isActive": false]


6. Troubleshooting

Common Issues and Solutions

Issue 1: Context has no changes but save() is called repeatedly

Solution: Always check context.hasChanges before saving.

if context.hasChanges {

    try context.save()

}

Issue 2: Thread violations and crashes

Symptoms: App crashes with "CoreData was not used on the correct thread"

Solution: Always use perform or performAndWait for context operations.

// Wrong

let results = try? context.fetch(fetchRequest)


// Correct

context.perform {

    let results = try? self.context.fetch(fetchRequest)

}

Issue 3: Faulting errors when accessing relationships

Symptoms: "CoreData could not fulfill a fault"

Solutions:

// Solution 1: Prefetch relationships

fetchRequest.relationshipKeyPathsForPrefetching = ["company", "employees"]


// Solution 2: Check if object is deleted

if !person.isDeleted {

    print(person.company?.name)

}


// Solution 3: Refresh object

context.refresh(person, mergeChanges: true)

Issue 4: Memory growing indefinitely

Symptoms: Memory usage increases continuously during data processing

Solution: Reset context periodically and use batch processing.

func processLargeDataset() {

    let context = CoreDataStack.shared.newBackgroundContext()

    

    context.perform {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.fetchBatchSize = 100

        

        do {

            let people = try context.fetch(fetchRequest)

            

            for (index, person) in people.enumerated() {

                // Process person

                

                if index % 100 == 0 {

                    try context.save()

                    context.reset() // Clear memory

                }

            }

            

            try context.save()

        } catch {

            print("Error: \(error)")

        }

    }

}

Issue 5: Merge conflicts between contexts

Symptoms: Data inconsistencies or crashes when multiple contexts modify same data

Solution: Set appropriate merge policy.

// Last write wins (object properties)

context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy


// Store data wins

context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy


// Overwrite everything

context.mergePolicy = NSOverwriteMergePolicy


// Rollback on conflict

context.mergePolicy = NSRollbackMergePolicy

Issue 6: Migration fails with no error message

Solution: Enable verbose logging and check model compatibility.

// Enable SQL debugging (add to scheme arguments)

-com.apple.CoreData.SQLDebug 1


// Check compatibility

let sourceMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(

    ofType: NSSQLiteStoreType,

    at: storeURL,

    options: nil

)


let isCompatible = managedObjectModel.isConfiguration(

    withName: nil,

    compatibleWithStoreMetadata: sourceMetadata ?? [:]

)


print("Store is compatible: \(isCompatible)")

Issue 7: Slow fetch performance

Symptoms: Fetches taking several seconds

Solutions:

// Add indexes in data model

// Select attribute > Data Model Inspector > Check "Indexed"


// Use batch sizes

fetchRequest.fetchBatchSize = 20


// Optimize predicates

// Bad: MATCHES with regex

NSPredicate(format: "name MATCHES %@", ".*John.*")


// Good: CONTAINS

NSPredicate(format: "name CONTAINS[c] %@", "John")


// Fetch only needed properties

fetchRequest.propertiesToFetch = ["firstName", "lastName"]

fetchRequest.resultType = .dictionaryResultType

Issue 8: Unique constraint violations

Symptoms: "Constraint violation" errors when saving

Solution: Use unique constraints or check before inserting.

// In data model: select attribute > check "Unique"


// Or check programmatically

let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

fetchRequest.predicate = NSPredicate(format: "email == %@", email)

fetchRequest.fetchLimit = 1


if let existing = try? context.fetch(fetchRequest).first {

    // Update existing

    existing.firstName = firstName

} else {

    // Create new

    let person = Person(context: context)

    person.email = email

    person.firstName = firstName

}


try? context.save()

Issue 9: Binary data consuming too much memory

Symptoms: Large memory spikes when loading objects with binary data

Solution: Use external storage for large binary data.

// In data model:

// 1. Select Binary Data attribute

// 2. Check "Allows External Storage"

// 3. This automatically stores large data outside the SQLite file


// For manual control

let data = largeImageData

person.imageData = data // Core Data handles external storage automatically

Issue 10: Undo manager affecting performance

Symptoms: Slow saves and high memory usage

Solution: Disable undo manager if not needed.

context.undoManager = nil

Detecting Store Corruption

func verifyStore(at url: URL) -> Bool {

    do {

        let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(

            ofType: NSSQLiteStoreType,

            at: url,

            options: nil

        )

        

        let model = CoreDataStack.shared.persistentContainer.managedObjectModel

        return model.isConfiguration(

            withName: nil, 

            compatibleWithStoreMetadata: metadata

        )

    } catch {

        print("Store verification failed: \(error)")

        return false

    }

}


func recoverFromCorruption(storeURL: URL) {

    // Backup corrupted store

    let backupURL = storeURL.appendingPathExtension("corrupted")

    try? FileManager.default.moveItem(at: storeURL, to: backupURL)

    

    // Create fresh store

    let container = NSPersistentContainer(name: "MyApp")

    container.loadPersistentStores { description, error in

        if error == nil {

            print("Fresh store created")

            // Optionally attempt data recovery from backup

        }

    }

}

Debugging Tools

Enable Core Data SQL Logging

// Add to scheme arguments in Xcode

-com.apple.CoreData.SQLDebug 1


// Or in code (before loading stores)

UserDefaults.standard.set(["com.apple.CoreData.SQLDebug": "1"], 

                          forKey: "com.apple.CoreData.SQLDebug")

Enable Threading Assertions

// Add to scheme arguments

-com.apple.CoreData.ConcurrencyDebug 1

Enable Migration Debug

// Add to scheme arguments

-com.apple.CoreData.MigrationDebug 1

Using Instruments

  1. Open Instruments (Cmd + I in Xcode)
  2. Select "Core Data" instrument
  3. Profile your app
  4. Analyze faults, fetches, and saves
  5. Monitor memory usage with "Allocations" and "Leaks"

Store File Inspection

func printStoreLocation() {

    let container = CoreDataStack.shared.persistentContainer

    if let storeURL = container.persistentStoreDescriptions.first?.url {

        print("Core Data Store: \(storeURL.path)")

    }

}


// Access store file for inspection

// Terminal commands:

// sqlite3 /path/to/store.sqlite

// .tables              # List all tables

// .schema ZTABLENAME   # Show table structure

// SELECT * FROM ZTABLENAME;  # Query data

Debug Fetch Requests

func debugFetchRequest<T: NSManagedObject>(_ fetchRequest: NSFetchRequest<T>) {

    print("Entity: \(fetchRequest.entityName ?? "Unknown")")

    print("Predicate: \(fetchRequest.predicate?.description ?? "None")")

    print("Sort Descriptors: \(fetchRequest.sortDescriptors?.description ?? "None")")

    print("Fetch Limit: \(fetchRequest.fetchLimit)")

    print("Fetch Offset: \(fetchRequest.fetchOffset)")

    print("Batch Size: \(fetchRequest.fetchBatchSize)")

    print("Returns Objects As Faults: \(fetchRequest.returnsObjectsAsFaults)")

    

    let startTime = Date()

    do {

        let results = try context.fetch(fetchRequest)

        let duration = Date().timeIntervalSince(startTime)

        print("Fetch completed in \(duration)s, returned \(results.count) objects")

    } catch {

        print("Fetch failed: \(error)")

    }

}

Common Error Messages

"The model used to open the store is incompatible"

  • Solution: Run migration or delete and recreate store

"Object's persistent store is not reachable"

  • Solution: Object was deleted or store was removed

"Illegal attempt to establish a relationship"

  • Solution: Ensure both objects belong to same context

"Context already has a coordinator"

  • Solution: Don't reassign coordinator after it's set

"Optimistic locking failure"

  • Solution: Handle merge conflicts with appropriate merge policy

Best Debugging Practices

  1. Enable all Core Data debugging flags during development
  2. Use breakpoints in save operations
  3. Log all Core Data operations in debug builds
  4. Monitor memory usage regularly
  5. Test with large datasets
  6. Test migration paths thoroughly
  7. Use unit tests for data operations
  8. Profile with Instruments regularly


Conclusion

This guide covered the advanced aspects of Core Data including concurrency management, migration strategies, performance optimization techniques, advanced architectural patterns, best practices, and comprehensive troubleshooting.

Key Takeaways

  • Concurrency: Always use contexts on appropriate queues
  • Migration: Test thoroughly and use progressive migration for complex changes
  • Performance: Optimize fetch requests, use indexing, and batch operations
  • Patterns: Implement Repository and Service layers for clean architecture
  • Best Practices: Validate data, handle errors properly, and write tests
  • Troubleshooting: Enable debugging tools and monitor performance

Next Steps

  • Explore SwiftUI integration with @FetchRequest
  • Learn CloudKit sync with Core Data
  • Study Combine framework integration
  • Investigate Core Spotlight for search
  • Consider SwiftData for iOS 17+ projects

Master these advanced topics to build robust, performant, and maintainable Core Data applications.() { setupContextNotifications() }

private func setupContextNotifications() {

    NotificationCenter.default.addObserver(

        self,

        selector: #selector(contextDidSave(_:)),

        name: .NSManagedObjectContextDidSave,

        object: nil

    )

}


@objc private func contextDidSave(_ notification: Notification) {

    guard let context = notification.object as? NSManagedObjectContext else {

        return

    }

    

    // Don't merge changes from the main context into itself

    let mainContext = CoreDataStack.shared.mainContext

    if context != mainContext {

        mainContext.perform {

            mainContext.mergeChanges(fromContextDidSave: notification)

        }

    }

}

}

// Automatic merging (iOS 10+) let container = NSPersistentContainer(name: "MyApp") container.viewContext.automaticallyMergesChangesFromParent = true

### Batch Operations (Non-Context)


Batch operations bypass the context for better performance but don't update objects in memory.


```swift

// Batch update

func batchUpdateInactiveUsers() {

    let context = CoreDataStack.shared.mainContext

    let batchUpdate = NSBatchUpdateRequest(entityName: "Person")

    batchUpdate.predicate = NSPredicate(format: "lastLoginDate < %@", thirtyDaysAgo as NSDate)

    batchUpdate.propertiesToUpdate = ["isActive": false]

    batchUpdate.resultType = .updatedObjectIDsResultType

    

    do {

        let result = try context.execute(batchUpdate) as? NSBatchUpdateResult

        let objectIDArray = result?.result as? [NSManagedObjectID] ?? []

        let changes = [NSUpdatedObjectsKey: objectIDArray]

        NSManagedObjectContext.mergeChanges(

            fromRemoteContextSave: changes,

            into: [context]

        )

    } catch {

        print("Batch update failed: \(error)")

    }

}


// Batch delete

func batchDeleteOldRecords() {

    let context = CoreDataStack.shared.mainContext

    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Person.fetchRequest()

    fetchRequest.predicate = NSPredicate(

        format: "createdDate < %@",

        oneYearAgo as NSDate

    )

    

    let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)

    batchDelete.resultType = .resultTypeObjectIDs

    

    do {

        let result = try context.execute(batchDelete) as? NSBatchDeleteResult

        let objectIDArray = result?.result as? [NSManagedObjectID] ?? []

        let changes = [NSDeletedObjectsKey: objectIDArray]

        NSManagedObjectContext.mergeChanges(

            fromRemoteContextSave: changes,

            into: [context]

        )

    } catch {

        print("Batch delete failed: \(error)")

    }

}

Concurrency Best Practices

Always use perform or performAndWait for context operations. Never share managed objects between threads or contexts. Use object IDs to reference objects across contexts. Enable automatic merging for better sync between contexts. Use background contexts for imports and heavy processing. Always dispatch UI updates to the main queue.


2. Migration and Versioning

When You Need Migration

You need migration when:

  • Adding or removing entities or attributes
  • Changing attribute types
  • Modifying relationships
  • Renaming entities or attributes
  • Changing entity hierarchies

Always create a new model version before making changes to production data.

Creating Model Versions

In Xcode:

  1. Select your .xcdatamodeld file
  2. Go to Editor > Add Model Version
  3. Name the new version (usually v2, v3, etc.)
  4. Make your changes in the new version
  5. Set the new version as current in the model inspector

Lightweight Migration

Lightweight migration handles simple changes automatically, including:

  • Adding or removing attributes (with default values)
  • Adding or removing entities
  • Renaming attributes or entities (with renaming identifier)
  • Changing optional to required (with default value)

// Enable lightweight migration

lazy var persistentContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: "MyApp")

    

    let description = container.persistentStoreDescriptions.first

    description?.shouldMigrateStoreAutomatically = true

    description?.shouldInferMappingModelAutomatically = true

    

    container.loadPersistentStores { description, error in

        if let error = error {

            fatalError("Unable to load persistent stores: \(error)")

        }

    }

    

    return container

}()


// Legacy approach

let options = [

    NSMigratePersistentStoresAutomaticallyOption: true,

    NSInferMappingModelAutomaticallyOption: true

]


try coordinator.addPersistentStore(

    ofType: NSSQLiteStoreType,

    configurationName: nil,

    at: storeURL,

    options: options

)

Renaming Attributes

Set renaming identifier in the model editor:

  1. Select the renamed attribute
  2. In the inspector, set the Renaming ID to the old name
  3. This tells Core Data that "newName" is actually "oldName" renamed

Manual Migration

For complex changes, create a custom mapping model.

// Create mapping model

// File > New > File > Mapping Model

// Select source and destination models


// Custom migration policy

import CoreData


class PersonToEmployeeMigrationPolicy: NSEntityMigrationPolicy {

    override func createDestinationInstances(

        forSource sInstance: NSManagedObject,

        in mapping: NSEntityMapping,

        manager: NSMigrationManager

    ) throws {

        try super.createDestinationInstances(

            forSource: sInstance,

            in: mapping,

            manager: manager

        )

        

        guard let destObject = manager.destinationInstances(

            forEntityMappingName: mapping.name,

            sourceInstances: [sInstance]

        ).first else {

            return

        }

        

        // Custom migration logic

        if let fullName = sInstance.value(forKey: "fullName") as? String {

            let components = fullName.components(separatedBy: " ")

            destObject.setValue(components.first, forKey: "firstName")

            destObject.setValue(components.last, forKey: "lastName")

        }

    }

}


// Perform migration

func migrateStore(at storeURL: URL) {

    let sourceModel = NSManagedObjectModel(

        contentsOf: Bundle.main.url(

            forResource: "MyApp",

            withExtension: "momd/MyApp.mom"

        )!

    )!

    

    let destinationModel = NSManagedObjectModel(

        contentsOf: Bundle.main.url(

            forResource: "MyApp",

            withExtension: "momd/MyApp 2.mom"

        )!

    )!

    

    let mappingModel = NSMappingModel(

        from: [Bundle.main],

        forSourceModel: sourceModel,

        destinationModel: destinationModel

    )!

    

    let manager = NSMigrationManager(

        sourceModel: sourceModel,

        destinationModel: destinationModel

    )

    

    let destURL = storeURL.deletingLastPathComponent()

        .appendingPathComponent("MyApp_new.sqlite")

    

    try? manager.migrateStore(

        from: storeURL,

        sourceType: NSSQLiteStoreType,

        options: nil,

        with: mappingModel,

        toDestinationURL: destURL,

        destinationType: NSSQLiteStoreType,

        destinationOptions: nil

    )

    

    // Replace old store with new one

    try? FileManager.default.removeItem(at: storeURL)

    try? FileManager.default.moveItem(at: destURL, to: storeURL)

}

Progressive Migration

For apps with multiple versions, migrate progressively through each version.

func progressiveMigration(from sourceURL: URL, to destinationURL: URL) throws {

    let sourceMetadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(

        ofType: NSSQLiteStoreType,

        at: sourceURL,

        options: nil

    )

    

    let modelVersions = ["MyApp", "MyApp 2", "MyApp 3", "MyApp 4"]

    var currentURL = sourceURL

    

    for (index, version) in modelVersions.enumerated() {

        guard let model = NSManagedObjectModel(

            contentsOf: Bundle.main.url(

                forResource: version,

                withExtension: "mom",

                subdirectory: "MyApp.momd"

            )!

        ) else {

            continue

        }

        

        if model.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata) {

            // This is our source model

            continue

        }

        

        // Migrate to this version

        let nextURL = destinationURL.deletingLastPathComponent()

            .appendingPathComponent("temp_\(index).sqlite")

        

        try migrateStore(from: currentURL, to: nextURL, targetVersion: version)

        currentURL = nextURL

    }

    

    // Final move

    try FileManager.default.moveItem(at: currentURL, to: destinationURL)

}

Testing Migration

func testMigration() {

    // Create test store with old model

    let oldStoreURL = FileManager.default.temporaryDirectory

        .appendingPathComponent("test_old.sqlite")

    

    // Populate with test data

    let oldContainer = NSPersistentContainer(name: "MyApp")

    // ... add test data ...

    

    // Attempt migration

    let newContainer = NSPersistentContainer(name: "MyApp")

    let description = NSPersistentStoreDescription(url: oldStoreURL)

    description.shouldMigrateStoreAutomatically = true

    description.shouldInferMappingModelAutomatically = true

    newContainer.persistentStoreDescriptions = [description]

    

    newContainer.loadPersistentStores { description, error in

        if let error = error {

            XCTFail("Migration failed: \(error)")

        } else {

            print("Migration successful")

        }

    }

}

Migration Best Practices

Always test migrations with production data copies. Create backups before migrating. Use progressive migration for multiple versions. Provide user feedback during long migrations. Handle migration failures gracefully. Document all model changes. Use version numbers in model names.


3. Performance Optimization

Fetch Request Optimization

// BAD: Fetching all objects

let allPeople = try? context.fetch(Person.fetchRequest())


// GOOD: Use batch sizes

let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

fetchRequest.fetchBatchSize = 20


// GOOD: Fetch only needed properties

fetchRequest.propertiesToFetch = ["firstName", "lastName"]

fetchRequest.resultType = .dictionaryResultType


// GOOD: Use faulting strategically

fetchRequest.returnsObjectsAsFaults = false // Load objects immediately

// OR

fetchRequest.returnsObjectsAsFaults = true // Load as faults (default)


// GOOD: Prefetch relationships

fetchRequest.relationshipKeyPathsForPrefetching = ["company", "projects"]

Batch Faulting

// Fetch with relationships efficiently

let fetchRequest: NSFetchRequest<Company> = Company.fetchRequest()

fetchRequest.relationshipKeyPathsForPrefetching = ["employees"]

fetchRequest.fetchBatchSize = 20


let companies = try? context.fetch(fetchRequest)


// Now accessing employees is efficient

companies?.forEach { company in

    // Already prefetched, no additional fetch

    print("\(company.name): \(company.employees?.count ?? 0) employees")

}

Using Faulting Strategically

// For large lists where details aren't immediately needed

fetchRequest.returnsObjectsAsFaults = true

let people = try? context.fetch(fetchRequest)


// Objects are loaded on demand

people?.forEach { person in

    // Properties accessed here trigger fault firing

    print(person.firstName)

}


// For immediate use of all data

fetchRequest.returnsObjectsAsFaults = false

let peopleFullyLoaded = try? context.fetch(fetchRequest)


// All data immediately available, no lazy loading

Memory Management

// Process large datasets in batches

func processLargeDataset() {

    let context = CoreDataStack.shared.newBackgroundContext()

    

    context.perform {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.fetchBatchSize = 100

        

        do {

            let people = try context.fetch(fetchRequest)

            

            for (index, person) in people.enumerated() {

                // Process person

                person.processData()

                

                // Reset context every 100 objects

                if index % 100 == 0 {

                    try context.save()

                    context.reset() // Clear memory

                }

            }

            

            // Final save

            try context.save()

        } catch {

            print("Error: \(error)")

        }

    }

}

Asynchronous Fetch Requests

func performAsyncFetch() {

    let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

    

    let asyncFetchRequest = NSAsynchronousFetchRequest(

        fetchRequest: fetchRequest

    ) { result in

        guard let people = result.finalResult else { return }

        

        DispatchQueue.main.async {

            // Update UI with results

            self.updateUI(with: people)

        }

    }

    

    do {

        try context.execute(asyncFetchRequest)

    } catch {

        print("Async fetch failed: \(error)")

    }

}

Persistent History Tracking

For apps with multiple contexts or sync requirements, track changes over time.

// Enable persistent history

let description = container.persistentStoreDescriptions.first

description?.setOption(

    true as NSNumber,

    forKey: NSPersistentHistoryTrackingKey

)


// Fetch changes since last sync

func fetchChangesSinceLastSync(token: NSPersistentHistoryToken?) {

    let context = CoreDataStack.shared.newBackgroundContext()

    let request = NSPersistentHistoryChangeRequest.fetchHistory(after: token)

    

    do {

        let result = try context.execute(request) as? NSPersistentHistoryResult

        guard let transactions = result?.result as? [NSPersistentHistoryTransaction] else {

            return

        }

        

        for transaction in transactions {

            // Process changes

            processTransaction(transaction)

        }

        

        // Save last token

        if let lastToken = transactions.last?.token {

            saveToken(lastToken)

        }

    } catch {

        print("Failed to fetch history: \(error)")

    }

}

Indexing

Add indexes to frequently queried attributes.

// In data model editor:

// Select attribute > Data Model Inspector

// Check "Indexed" for frequently searched attributes


// Good candidates for indexing:

// - Primary keys or unique identifiers

// - Attributes used in WHERE clauses

// - Attributes used for sorting

// - Foreign keys in relationships


// Note: Indexes speed up reads but slow down writes

// Only index attributes that are queried frequently

Query Performance Tips

// Count without fetching

let count = try? context.count(for: fetchRequest)


// Fetch object IDs only

fetchRequest.resultType = .managedObjectIDResultType

let objectIDs = try? context.fetch(fetchRequest) as? [NSManagedObjectID]


// Use batch limits for pagination

fetchRequest.fetchLimit = 50

fetchRequest.fetchOffset = currentPage * 50


// Avoid expensive predicates

// BAD: Regex or complex string operations

NSPredicate(format: "name MATCHES %@", ".*John.*")


// GOOD: Simple comparisons

NSPredicate(format: "name CONTAINS[c] %@", "John")

Performance Monitoring

class CoreDataPerformanceMonitor {

    static let shared = CoreDataPerformanceMonitor()

    

    private init() {

        NotificationCenter.default.addObserver(

            self,

            selector: #selector(contextWillSave(_:)),

            name: .NSManagedObjectContextWillSave,

            object: nil

        )

        

        NotificationCenter.default.addObserver(

            self,

            selector: #selector(contextDidSave(_:)),

            name: .NSManagedObjectContextDidSave,

            object: nil

        )

    }

    

    private var saveStartTime: Date?

    

    @objc private func contextWillSave(_ notification: Notification) {

        saveStartTime = Date()

    }

    

    @objc private func contextDidSave(_ notification: Notification) {

        guard let startTime = saveStartTime else { return }

        

        let duration = Date().timeIntervalSince(startTime)

        if duration > 0.1 { // Warn if save takes more than 100ms

            print("⚠️ Slow save detected: \(duration)s")

            

            if let userInfo = notification.userInfo {

                let inserted = (userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>)?.count ?? 0

                let updated = (userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>)?.count ?? 0

                let deleted = (userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>)?.count ?? 0

                

                print("  Inserted: \(inserted), Updated: \(updated), Deleted: \(deleted)")

            }

        }

        

        saveStartTime = nil

    }

}


4. Advanced Patterns

Repository Pattern

Separate Core Data logic from the rest of your app.

protocol PersonRepository {

    func create(firstName: String, lastName: String, age: Int) -> Person?

    func fetch(predicate: NSPredicate?) -> [Person]

    func update(person: Person, changes: [String: Any]) -> Bool

    func delete(person: Person) -> Bool

}


class CoreDataPersonRepository: PersonRepository {

    private let context: NSManagedObjectContext

    

    init(context: NSManagedObjectContext = CoreDataStack.shared.mainContext) {

        self.context = context

    }

    

    func create(firstName: String, lastName: String, age: Int) -> Person? {

        let person = Person(context: context)

        person.firstName = firstName

        person.lastName = lastName

        person.age = Int16(age)

        

        do {

            try context.save()

            return person

        } catch {

            print("Failed to create: \(error)")

            return nil

        }

    }

    

    func fetch(predicate: NSPredicate? = nil) -> [Person] {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.predicate = predicate

        

        return (try? context.fetch(fetchRequest)) ?? []

    }

    

    func update(person: Person, changes: [String: Any]) -> Bool {

        for (key, value) in changes {

            person.setValue(value, forKey: key)

        }

        

        do {

            try context.save()

            return true

        } catch {

            print("Failed to update: \(error)")

            return false

        }

    }

    

    func delete(person: Person) -> Bool {

        context.delete(person)

        

        do {

            try context.save()

            return true

        } catch {

            print("Failed to delete: \(error)")

            return false

        }

    }

}


// Usage in ViewModel or Controller

class PeopleViewModel {

    private let repository: PersonRepository

    

    init(repository: PersonRepository = CoreDataPersonRepository()) {

        self.repository = repository

    }

    

    func loadPeople() -> [Person] {

        return repository.fetch(predicate: nil)

    }

    

    func addPerson(firstName: String, lastName: String, age: Int) {

        _ = repository.create(firstName: firstName, lastName: lastName, age: age)

    }

}

Unit of Work Pattern

Group related changes together.

class UnitOfWork {

    private let context: NSManagedObjectContext

    private var operations: [() -> Void] = []

    

    init(context: NSManagedObjectContext) {

        self.context = context

    }

    

    func register(operation: @escaping () -> Void) {

        operations.append(operation)

    }

    

    func commit() -> Result<Void, Error> {

        operations.forEach { $0() }

        

        do {

            try context.save()

            operations.removeAll()

            return .success(())

        } catch {

            context.rollback()

            return .failure(error)

        }

    }

    

    func rollback() {

        context.rollback()

        operations.removeAll()

    }

}


// Usage

let unitOfWork = UnitOfWork(context: context)


unitOfWork.register {

    let person = Person(context: self.context)

    person.firstName = "John"

}


unitOfWork.register {

    let company = Company(context: self.context)

    company.name = "Tech Corp"

}


switch unitOfWork.commit() {

case .success:

    print("All changes saved")

case .failure(let error):

    print("Failed: \(error)")

}

Generic Core Data Manager

protocol ManagedObjectConvertible {

    associatedtype ManagedObject: NSManagedObject

    func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject

}


class CoreDataManager<T: NSManagedObject> {

    private let context: NSManagedObjectContext

    

    init(context: NSManagedObjectContext = CoreDataStack.shared.mainContext) {

        self.context = context

    }

    

    func fetch(

        predicate: NSPredicate? = nil,

        sortDescriptors: [NSSortDescriptor]? = nil,

        fetchLimit: Int? = nil

    ) -> [T] {

        let fetchRequest = NSFetchRequest<T>(entityName: String(describing: T.self))

        fetchRequest.predicate = predicate

        fetchRequest.sortDescriptors = sortDescriptors

        if let limit = fetchLimit {

            fetchRequest.fetchLimit = limit

        }

        

        return (try? context.fetch(fetchRequest)) ?? []

    }

    

    func create() -> T {

        return T(context: context)

    }

    

    func save() -> Bool {

        guard context.hasChanges else { return true }

        

        do {

            try context.save()

            return true

        } catch {

            print("Save failed: \(error)")

            return false

        }

    }

    

    func delete(_ object: T) -> Bool {

        context.delete(object)

        return save()

    }

    

    func deleteAll() -> Bool {

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(

            entityName: String(describing: T.self)

        )

        let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)

        

        do {

            try context.execute(batchDelete)

            return true

        } catch {

            print("Batch delete failed: \(error)")

            return false

        }

    }

}


// Usage

let personManager = CoreDataManager<Person>()

let people = personManager.fetch(

    predicate: NSPredicate(format: "age > %d", 18),

    sortDescriptors: [NSSortDescriptor(key: "lastName", ascending: true)]

)


let newPerson = personManager.create()

newPerson.firstName = "Alice"

personManager.save()

Sync Manager Pattern

For syncing with remote servers.

class SyncManager {

    private let context: NSManagedObjectContext

    private let apiClient: APIClient

    

    init() {

        self.context = CoreDataStack.shared.newBackgroundContext()

        self.apiClient = APIClient()

    }

    

    func syncPeople() async throws {

        // Fetch from server

        let remotePeople = try await apiClient.fetchPeople()

        

        // Update local database

        context.perform {

            for remoteData in remotePeople {

                let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

                fetchRequest.predicate = NSPredicate(

                    format: "remoteID == %@",

                    remoteData.id

                )

                

                let person: Person

                if let existing = try? self.context.fetch(fetchRequest).first {

                    person = existing

                } else {

                    person = Person(context: self.context)

                    person.remoteID = remoteData.id

                }

                

                person.firstName = remoteData.firstName

                person.lastName = remoteData.lastName

                person.age = Int16(remoteData.age)

                person.lastSyncDate = Date()

            }

            

            do {

                try self.context.save()

            } catch {

                print("Sync failed: \(error)")

            }

        }

    }

    

    func pushLocalChanges() async throws {

        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        fetchRequest.predicate = NSPredicate(

            format: "lastModifiedDate > lastSyncDate OR lastSyncDate == nil"

        )

        

        let changedPeople = try context.fetch(fetchRequest)

        

        for person in changedPeople {

            let data = PersonDTO(from: person)

            try await apiClient.updatePerson(data)

            person.lastSyncDate = Date()

        }

        

        try context.save()

    }

}

Service Layer Pattern

protocol DataService {

    associatedtype Entity: NSManagedObject

    func getAll() async throws -> [Entity]

    func getById(_ id: NSManagedObjectID) async throws -> Entity?

    func create(_ object: Entity) async throws

    func update(_ object: Entity) async throws

    func delete(_ object: Entity) async throws

}


class PersonService: DataService {

    typealias Entity = Person

    

    private let context: NSManagedObjectContext

    

    init

Comments

Popular posts from this blog

Complete iOS Developer Guide - Swift 5 by @hiren_syl |  You Must Have To Know 😎 

piano online keyboard

Higher-Order Functions in Swift