Chapters

Hide chapters

Concurrency by Tutorials

Third Edition · iOS 16 · Swift 5.7 · Xcode 14

11. Core Data
Written by Scott Grosch

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

Many of the iOS apps you’ll develop are likely to use Core Data for data storage. While mixing concurrency and Core Data is no longer complicated in modern versions of iOS, there are still a couple of key concepts that you’ll want to be aware of. Just like most of UIKit, Core Data is not thread safe.

Note: This chapter assumes that you already know how to perform basic Core Data concepts such as searching, creating entities, etc… Please check out our book, Core Data by Tutorials if you are new to Core Data.

NSManagedObjectContext Is Not Thread Safe

The NSManagedObjectContext, which gets created as part of an NSPersistentContainer from your AppDelegate, is tied to the main thread. As you’ve learned throughout the rest of this book, this means you can only use that context on the main UI thread. However, if you do so, you’re going to negatively impact your user’s experience.

There are two methods available on the NSManagedObjectContext class to help with concurrency:

  • perform(_:)
  • performAndWait(_:)

What both methods do is ensure that whatever action you pass to the closure is executed on the same queue that created the context. Notice how it’s not “on the main thread.” You might create your own context on another queue. These methods thus ensure that you are always executing against the proper thread and don’t crash at runtime.

The only difference between the two is that the first is an asynchronous method, whereas the second is synchronous. It should be abnormal for you to perform Core Data tasks without utilizing either of these methods. Even though you might know you’re on the proper thread already, you might refactor your code in the future and forget to wrap the Core Data calls at that point in time. Save yourself the headache and use them right from the start!

Importing Data

When your app starts, one of the first goals it frequently has is to contact the server and download any new data. Once the network operation completes, an expensive compare and import cycle will have to take place. However, you don’t need to create an entire Operation for this common task. Core Data’s NSPersistentContainer provides performBackgroundTask(_:) which will help you out:

PersistenceController.shared.container.performBackgroundTask { context in
  for json in jsonDataFromServer {
    let obj = MyEntity(context: context)
    obj.populate(from: json)
  }

  do {
    try context.save()
  } catch {
    fatalError("Failed to save context")
  }
}
let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
childContext.parent = persistentContainer.viewContext

childContext.perform {
  ...
}

Using NSAsynchronousFetchRequest

When you use a FetchRequest to query Core Data, the operation is synchronous. If you’re just grabbing a single object, that’s perfectly acceptable. When you’re performing a time-consuming query, such as retrieving data to populate a List, then you’ll prefer to perform the query asynchronously. Using NSAsynchronousFetchRequest is the obvious solution.

let ageKeyPath = #keyPath(Person.age)

let fetchRequest = Person.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K > 13", ageKeyPath)

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

  guard let people = result.finalResult else {
    return
  }

  DispatchQueue.main.async {
    self.people = people
  }
}

do {
  let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
  try backgroundContext.execute(asyncFetch)
} catch let error {
  // handle the error
}

Sharing an NSManagedObject

You can’t share an NSManagedObject — or a subclass — between threads. Sure, you can, and more often than not it will seem to work, but your app is going to break, so don’t do it!

let objectId = someEntity.objectID

DispatchQueue.main.async { [weak self] in
  guard let self else { return }

  let myEntity = self.managedObjectContext.object(with: objectId)
  self.address = myEntity.address
}

Using ConcurrencyDebug

To help protect yourself from sharing an NSManagedObject across threads, you can enable a runtime debug flag by editing the project’ scheme and passing a runtime argument.

private let passActualObject = true
private let passActualObject = false

Where to Go From Here?

Hopefully, the preceding few pages have shown you that it’s pretty easy to work with Core Data in a thread safe way. If you previously shied away from this great framework because it was hard to use concurrently, now is the time to take another look!

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