Highlight
Core Data fully embraces Swift 5.5 concurrency in iOS 15.
NSManagedObjectContextgains asyncperformoverloads supportingtry awaitreturns and thrown errors; SwiftUI’s@FetchRequestadds dynamic configuration and@SectionedFetchRequestfor grouped queries—making data-driven UI code more concise and safer.
Core Content
Why Core Data Needs Concurrency
Persisting data requires reading and writing external storage—naturally asynchronous. Core Data has focused on concurrent execution from the start, but past APIs (performAndWait, perform) required manual thread synchronization and result passing.
Swift 5.5’s structured concurrency model lets Core Data offer more natural APIs.
Pain Points of the Old Approach
With performAndWait, the calling thread blocks until the closure finishes. If you don’t need to wait, use perform—but manually pass results and errors via completion handlers.
// Past: manually pass errors
var fetchError: Error?
context.performAndWait {
do {
// ... fetch ...
} catch {
fetchError = error
}
}
if let error = fetchError {
// handle error
}
Problems:
- Mutable variables needed to capture results
- Error handling scattered across locations
- Hard to read and test
New async perform API
iOS 15 adds async perform overloads to NSManagedObjectContext:
let count = try await context.perform {
let request = Quake.fetchRequest()
let fiveHoursAgo = Calendar.current.date(byAdding: .hour, value: -5, to: Date())!
request.predicate = NSPredicate(format: "time >= %@", fiveHoursAgo as NSDate)
request.resultType = .countResultType
return try context.count(for: request)
}
Key points:
performclosures canthrow; callers usetry await- Closures can
returnvalues directly to the caller - Compiler handles thread switching and result routing
- No manual capture variables
Scheduling: immediate vs enqueued
New async perform supports two scheduling strategies:
.immediate (default):
- From a different execution context: waits for scheduling and runs
- From the same context: optimistically runs immediately
.enqueued:
- Always appends work to the context queue end
- Regardless of caller context
// Using enqueued strategy
let result = try await context.perform(schedule: .enqueued) {
// This code always runs at the end of the current queue
return try context.count(for: request)
}
Safe Return Value Boundaries
The new API makes returning values easy, but with an important limit: don’t return registered NSManagedObject across contexts.
// Dangerous: returning managed object to another context
let quake = try await backgroundContext.perform {
let request = Quake.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
request.fetchLimit = 1
return try backgroundContext.fetch(request).first // Don't do this!
}
Correct approaches:
- Use
NSManagedObjectID; re-fetch in the target context - Use fetch request dictionary result types
// Safe: return object ID
let objectID = try await backgroundContext.perform {
let request = Quake.fetchRequest()
request.resultType = .managedObjectIDResultType
request.fetchLimit = 1
return try backgroundContext.fetch(request).first as? NSManagedObjectID
}
// Re-fetch in view context
if let objectID = objectID {
let quake = viewContext.object(with: objectID)
}
Refactoring Import Flow
The Earthquakes sample shows async/await import refactoring:
func importQuakes(from data: Data) async throws {
let quakes = try JSONDecoder().decode([Quake].self, from: data)
try await backgroundContext.perform {
for quake in quakes {
let newQuake = Quake(context: backgroundContext)
newQuake.magnitude = quake.magnitude
newQuake.place = quake.place
newQuake.time = quake.time
}
try backgroundContext.save()
}
}
Key points:
- Entire import flow declared
async throws performhandles all Core Data operations inside- Errors propagate naturally upward
- Callers use
try awaitto wait for completion
Swift-Friendly API Improvements
Core Data is more Swift-friendly in iOS 15:
Persistent store types
// New Swift-style names
let storeType = NSPersistentStore.StoreType.sqlite
// Replaces old string "SQLite"
Attribute types
// New AttributeType enum
func assertAttribute(named name: String, on entity: NSEntityDescription, hasType type: NSAttributeDescription.AttributeType) {
guard let attribute = entity.attributesByName[name] else {
XCTFail("No attribute named \(name)")
return
}
XCTAssertEqual(attribute.type, type)
}
// Usage
assertAttribute(named: "magnitude", on: quakeEntity, hasType: .double)
assertAttribute(named: "place", on: quakeEntity, hasType: .string)
FetchRequest Enhancements in SwiftUI
Dynamic sorting
// [20:36](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1236)
private let sorts = [(
name: "Time",
descriptors: [SortDescriptor(\Quake.time, order: .reverse)]
), (
name: "Time",
descriptors: [SortDescriptor(\Quake.time, order: .forward)]
), (
name: "Magnitude",
descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)]
), (
name: "Magnitude",
descriptors: [SortDescriptor(\Quake.magnitude, order: .forward)]
)]
struct ContentView: View {
@FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
private var quakes: FetchedResults<Quake>
@State private var selectedSort = SelectedSort()
var body: some View {
List(quakes) { quake in
QuakeRow(quake: quake)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
SortMenu(selection: $selectedSort)
.onChange(of: selectedSort) { _ in
let sortBy = sorts[selectedSort.index]
quakes.sortDescriptors = sortBy.descriptors
}
}
}
}
}
Key points:
@FetchRequest’ssortDescriptorsis now mutable- Switch sorting via
quakes.sortDescriptors = ... - Swift’s
SortDescriptortype is type-safe
Dynamic predicates
// [21:33](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1293)
struct ContentView: View {
@FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
private var quakes: FetchedResults<Quake>
@State private var searchText = ""
var query: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
quakes.nsPredicate = newValue.isEmpty
? nil
: NSPredicate(format: "place CONTAINS %@", newValue)
}
}
var body: some View {
List(quakes) { quake in
QuakeRow(quake: quake)
}
.searchable(text: query)
}
}
Key points:
quakes.nsPredicatecan be modified dynamically- Works with
.searchablefor search - Clear predicate when search is empty to show all data
Grouped queries with SectionedFetchRequest
// [23:26](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1406)
extension Quake {
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d, yyyy"
return formatter
}()
@objc var day: String {
return dateFormatter.string(from: time)
}
}
struct ContentView: View {
@SectionedFetchRequest(
sectionIdentifier: \.day,
sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
private var quakes: SectionedFetchResults<String, Quake>
var body: some View {
List {
ForEach(quakes) { section in
Section(header: Text(section.id)) {
ForEach(section) { quake in
QuakeRow(quake: quake)
}
}
}
}
}
}
Key points:
@SectionedFetchRequestgroups automatically by specified propertysectionIdentifierpoints to the grouping key path- Returns
SectionedFetchResultssupportingForEach - Grouping property needs
@objcbecause Core Data relies on KVO
Dynamic grouping switch
// [24:56](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1496)
private let sorts = [(
name: "Time",
descriptors: [SortDescriptor(\Quake.time, order: .reverse)],
section: \Quake.day
), (
name: "Magnitude",
descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)],
section: \Quake.magnitude_str
)]
// Switch sorting and grouping
.onChange(of: selectedSort) { _ in
let sortBy = sorts[selectedSort.index]
let config = quakes
config.sectionIdentifier = sortBy.section
config.sortDescriptors = sortBy.descriptors
}
Key points:
sectionIdentifiercan be modified dynamically- Switching grouping reorganizes the entire list automatically
- Combine with animation modifiers for smooth transitions
Detailed Content
Debugging Tools
Sanitizers
Xcode’s Address Sanitizer and Thread Sanitizer help detect concurrency issues:
- Address Sanitizer: memory usage problems
- Thread Sanitizer: multithreaded data access problems
Enable in Scheme Editor Run settings.
Core Data concurrency debugging
Enable a special runtime flag for Core Data to verify correct lock and type usage:
// Add to launch arguments
-com.apple.CoreData.ConcurrencyDebug 1
async Support for NSPersistentContainer and NSPersistentStoreCoordinator
Besides NSManagedObjectContext, NSPersistentContainer and NSPersistentStoreCoordinator also gained similar async APIs:
// NSPersistentContainer async load
let container = NSPersistentContainer(name: "Earthquakes")
try await container.loadPersistentStores()
Core Takeaways
1. Refactor all Core Data background work to async/await
- What: Migrate
performAndWait+ manual error handling to new asyncperform - Why: 50%+ less code; compiler guarantees thread safety; natural error handling
- How: Start with import/export; gradually replace all background context operations
2. Build sortable, searchable lists in SwiftUI
- What: Use
@FetchRequestdynamic configuration for sorting and search - Why: No manual fetched results controller; concise code; automatic data change handling
- How: Use
quakes.sortDescriptorsandquakes.nsPredicateto modify query conditions
3. Build date-grouped data displays
- What: Use
@SectionedFetchRequestfor lists grouped by date, category, etc. - Why: One line replaces dozens of
NSFetchedResultsController+ section logic - How: Add
@objccomputed property on entity; pass to@SectionedFetchRequest’ssectionIdentifier
4. Establish type-safe model validation tests
- What: Use new
AttributeTypeenum in unit tests to verify runtime model matches Xcode model editor - Why: Catch model-change runtime issues early
- How: Write
assertAttributetests per entity checking names and types
5. Design safe cross-context data passing
- What: In async perform closures, return only
NSManagedObjectIDor dictionaries—not managed objects - Why: Avoid crashes and inconsistency from cross-context access
- How: Audit all async perform return values; ensure no managed object instances returned
Related Sessions
- Discover concurrency in SwiftUI — SwiftUI concurrency including
taskandrefreshable - Explore structured concurrency in Swift — Structured concurrency: Task, Actor, etc.
- What’s new in SwiftUI — SwiftUI overview including more
@FetchRequestand@SectionedFetchRequestusage
Comments
GitHub Issues · utterances