Chapters

Hide chapters

Core Data by Tutorials

Eighth Edition · iOS 14 · Swift 5.3 · Xcode 12

Core Data by Tutorials

Section 1: 11 chapters
Show chapters Hide chapters

3. The Core Data Stack
Written by Pietro Rea

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Until now, you’ve been relying on Xcode’s Core Data template. There’s nothing wrong with getting help from Xcode (that’s what it’s there for!). But if you really want to know how Core Data works, building your own Core Data stack is a must.

The stack is made up of four Core Data classes:

  • NSManagedObjectModel
  • NSPersistentStore
  • NSPersistentStoreCoordinator
  • NSManagedObjectContext

Of these four classes, you’ve only encountered NSManagedObjectContext so far in this book. But the other three were there behind the scenes the whole time, supporting your managed context.

In this chapter, you’ll learn the details of what these four classes do. Rather than rely on the default starter template, you’ll build your own Core Data stack; a customizable wrapper around these classes.

Getting started

The sample project for this chapter is a simple dog-walking app. This application lets you save the date and time of your dog walks in a simple table view. Use this app regularly and your pooch (and his bladder) will love you.

You’ll find the sample project DogWalk in the resources accompanying this book. Open DogWalk.xcodeproj, then build and run the starter project.

As you can see, the sample app is already a fully-working (albeit simple) prototype. Tapping on the plus (+) button on the top-right adds a new entry to the list of walks. The image represents the dog you’re currently walking, but otherwise does nothing.

The app has all the functionality it needs, except for one important feature: The list of walks doesn’t persist. If you terminate DogWalk and re-launch, your entire history is gone. How will you remember if you walked your pooch this morning?

Your task in this chapter is to save the list of walks in Core Data. If that sounds like something you’ve already done in Chapters 1 and 2, here’s the twist; you’ll be writing your own Core Data stack to understand what’s really going on under the hood!

Rolling your own Core Data stack

Knowing how the Core Data stack works is more than a nice to know. If you’re working with a more advanced setup, such as migrating data from an old persistent store, digging into the stack is essential.

The managed object model

The NSManagedObjectModel represents each object type in your app’s data model, the properties they can have, and the relationships between them. Other parts of the Core Data stack use the model to create objects, store properties and save data.

The persistent store

NSPersistentStore reads and writes data to whichever storage method you’ve decided to use. Core Data provides four types of NSPersistentStore out of the box: three atomic and one non-atomic.

The persistent store coordinator

NSPersistentStoreCoordinator is the bridge between the managed object model and the persistent store. It’s responsible for using the model and the persistent stores to do most of the hard work in Core Data. It understands the NSManagedObjectModel and knows how to send information to, and fetch information from, the NSPersistentStore.

The managed object context

On a day-to-day basis, you’ll work with NSManagedObjectContext the most out of the four stack components. You’ll probably only see the other three components when you need to do something more advanced with Core Data.

let managedContext = employee.managedObjectContext

The persistent store container

If you thought there were only four pieces to the Core Data stack, you’re in for a surprise! As of iOS 10, there’s a new class to orchestrate all four Core Data stack classes: the managed model, the store coordinator, the persistent store and the managed context.

Creating your stack object

Now you know what each component does, it’s time to return to DogWalk and implement your own Core Data stack.

import Foundation
import CoreData

class CoreDataStack {
  private let modelName: String
  
  init(modelName: String) {
    self.modelName = modelName
  }
  
  private lazy var storeContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: self.modelName)
    container.loadPersistentStores { _, error in
      if let error = error as NSError? {
        print("Unresolved error \(error), \(error.userInfo)")
      }
    }
    return container
  }()
}
lazy var managedContext: NSManagedObjectContext = {
  return self.storeContainer.viewContext
}()
func saveContext () {
  guard managedContext.hasChanges else { return }
  
  do {
    try managedContext.save()
  } catch let error as NSError {
    print("Unresolved error \(error), \(error.userInfo)")
  }
}
import CoreData
lazy var coreDataStack = CoreDataStack(modelName: "DogWalk")

Modeling your data

Now your shiny new Core Data stack is securely fastened to the main view controller, it’s time to create your data model.

Adding managed object subclasses

In the previous chapter, you learned how to create custom managed object subclasses for your Core Data entities. It’s more convenient to work this way, so this is what you’ll do for Dog and Walk as well.

import Foundation
import CoreData

extension Dog {
  
  @nonobjc public class func fetchRequest()
    -> NSFetchRequest<Dog> {
    return NSFetchRequest<Dog>(entityName: "Dog")
  }
  
  @NSManaged public var name: String?
  @NSManaged public var walks: NSOrderedSet?
}

// MARK: Generated accessors for walks
extension Dog {
  
  @objc(insertObject:inWalksAtIndex:)
  @NSManaged public func insertIntoWalks(_ value: Walk,
                                         at idx: Int)
  
  @objc(removeObjectFromWalksAtIndex:)
  @NSManaged public func removeFromWalks(at idx: Int)
  
  @objc(insertWalks:atIndexes:)
  @NSManaged public func insertIntoWalks(_ values: [Walk],
                                         at indexes: NSIndexSet)
  
  @objc(removeWalksAtIndexes:)
  @NSManaged public func removeFromWalks(at indexes: NSIndexSet)
  
  @objc(replaceObjectInWalksAtIndex:withObject:)
  @NSManaged public func replaceWalks(at idx: Int,
                                      with value: Walk)
  
  @objc(replaceWalksAtIndexes:withWalks:)
  @NSManaged public func replaceWalks(at indexes: NSIndexSet,
                                      with values: [Walk])
  
  @objc(addWalksObject:)
  @NSManaged public func addToWalks(_ value: Walk)
  
  @objc(removeWalksObject:)
  @NSManaged public func removeFromWalks(_ value: Walk)
  
  @objc(addWalks:)
  @NSManaged public func addToWalks(_ values: NSOrderedSet)
  
  @objc(removeWalks:)
  @NSManaged public func removeFromWalks(_ values: NSOrderedSet)
}

extension Dog : Identifiable {

}
import Foundation
import CoreData

extension Walk {
  
  @nonobjc public class func fetchRequest()
    -> NSFetchRequest<Walk> {
    return NSFetchRequest<Walk>(entityName: "Walk")
  }
  
  @NSManaged public var date: Date?
  @NSManaged public var dog: Dog?
}

extension Walk : Identifiable {

}

A walk down persistence lane

Now your setup is complete; your Core Data stack, your data model and your managed object subclasses. It’s time to convert DogWalk to use Core Data. You’ve done this several times before, so this should be an easy section for you.

var currentDog: Dog?
let dogName = "Fido"
let dogFetch: NSFetchRequest<Dog> = Dog.fetchRequest()
dogFetch.predicate = NSPredicate(format: "%K == %@",
                                 #keyPath(Dog.name), dogName)

do {
  let results = try coreDataStack.managedContext.fetch(dogFetch)
  if results.isEmpty {
    // Fido not found, create Fido
    currentDog = Dog(context: coreDataStack.managedContext)
    currentDog?.name = dogName
    coreDataStack.saveContext()
  } else {
    // Fido found, use Fido
    currentDog = results.first
  }
} catch let error as NSError {
  print("Fetch error: \(error) description: \(error.userInfo)")
}
func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
  currentDog?.walks?.count ?? 0
}
func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
  ) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: "Cell", for: indexPath)

  guard let walk = currentDog?.walks?[indexPath.row] as? Walk,
    let walkDate = walk.date as Date? else {
      return cell
  }

  cell.textLabel?.text = dateFormatter.string(from: walkDate)
  return cell
}
@IBAction func add(_ sender: UIBarButtonItem) {
  // walks.append(Date())
  tableView.reloadData()
}

@IBAction func add(_ sender: UIBarButtonItem) {
  // Insert a new Walk entity into Core Data
  let walk = Walk(context: coreDataStack.managedContext)
  walk.date = Date()

  // Insert the new Walk into the Dog's walks set
  if let dog = currentDog,
    let walks = dog.walks?.mutableCopy()
      as? NSMutableOrderedSet {
      walks.add(walk)
      dog.walks = walks
  }

  // Save the managed object context
  coreDataStack.saveContext()

  // Reload table view
  tableView.reloadData()
}

Deleting objects from Core Data

Let’s say you were too trigger-friendly and tapped the plus (+) button when you didn’t mean to. You didn’t actually walk your dog, so you want to delete the walk you just added.

func tableView(_ tableView: UITableView,
               canEditRowAt indexPath: IndexPath) -> Bool {
  true
}
func tableView(
  _ tableView: UITableView,
  commit editingStyle: UITableViewCell.EditingStyle,
  forRowAt indexPath: IndexPath
) {

  //1
  guard let walkToRemove =
    currentDog?.walks?[indexPath.row] as? Walk,
    editingStyle == .delete else {
      return
  }

  //2
  coreDataStack.managedContext.delete(walkToRemove)

  //3
  coreDataStack.saveContext()

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

Key points

  • The Core Data stack is made up of five classes: NSManagedObjectModel, NSPersistentStore, NSPersistentStoreCoordinator, NSManagedObjectContext and the NSPersistentContainer that holds everything together.
  • The managed object model represents each object type in your app’s data model, the properties they can have, and the relationship between them.
  • A persistent store can be backed by a SQLite database (the default), XML, a binary file or in-memory store. You can also provide your own backing store with the incremental store API.
  • The persistent store coordinator hides the implementation details of how your persistent stores are configured and presents a simple interface for your managed object context.
  • The managed object context manages the lifecycles of the managed objects it creates or fetches. They are responsible for fetching, editing and deleting managed objects, as well as more powerful features such as validation, faulting and inverse relationship handling.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now