How to use CoreData safely

Issue #686

I now use Core Data more often now. Here is how I usually use it, for example in Push Hero

From iOS 10 and macOS 10.12, NSPersistentContainer that simplifies Core Data setup quite a lot. I usually use 1 NSPersistentContainer and its viewContext together with newBackgroundContext attached to that NSPersistentContainer

In Core Data, each context has a queue, except for viewContext using the DispatchQueue.main, and each NSManagedObject retrieved from 1 context is supposed to use within that context queue only, except for objectId property.

Although NSManagedObject subclasses from NSObject, it has a lot of other constraints that we need to be aware of. So it’s safe to treat Core Data as a cache layer, and use our own model on top of it. I usually perform operations on background context to avoid main thread blocking, and automaticallyMergesChangesFromParent handles merge changes automatically for us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
extension SendHistoryItem {
func toCoreData(context: NSManagedObjectContext) {
context.perform {
let cd = CDSendHistoryItem(context: context)
}
}
}

extension CDSendHistoryItem {
func toModel() throws -> SendHistoryItem {

}
}

final class CoreDataManager {
private var backgroundContext: NSManagedObjectContext?

init() {
self.backgroundContext = self.persistentContainer.newBackgroundContext()
}

lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "PushHero")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
print(error)
}
})
return container
}()

func load(request: NSFetchRequest<CDSendHistoryItem>, completion: @escaping ([SendHistoryItem]) -> Void) {
guard let context = CoreDataManager.shared.backgroundContext else { return }
context.perform {
do {
let cdItems = try request.execute()
let items = cdItems.compactMap({ try? $0.toModel() })
completion(items)
} catch {
completion([])
}
}
}

func save(items: [SendHistoryItem]) {
guard let context = backgroundContext else {
return
}

context.perform {
items.forEach {
let _ = $0.toCoreData(context: context)
}
do {
try context.save()
} catch {
print(error)
}
}
}
}

Read more

How to force FetchRequest update in SwiftUI

Issue #623

Listen to context changes notification and change SwiftUI View state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let changes = [NSDeletedObjectsKey: ids]
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: changes,
into: [context]
)
try context.save()

struct ListView: View {
@Environment(\.managedObjectContext)
var context

private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
@State
private var refreshing: Bool = false

var body: some View {
makeContent()
.onReceive(didSave) { _ in
self.refreshing.toggle()
}
}
}

We need to actually use that State variable for it to have effect

1
2
3
4
5
if refreshing {
Text("")
} else {
Text("")
}

How to batch delete in Core Data

Issue #622

Read Implementing Batch Deletes

If the entities that are being deleted are not loaded into memory, there is no need to update your application after the NSBatchDeleteRequest has been executed. However, if you are deleting objects in the persistence layer and those entities are also in memory, it is important that you notify the application that the objects in memory are stale and need to be refreshed.

To do this, first make sure the resultType of the NSBatchDeleteRequest is set to NSBatchDeleteRequestResultType.resultTypeObjectIDs before the request is executed. When the request has completed successfully, the resulting NSPersistentStoreResult instance that is returned will have an array of NSManagedObjectID instances referenced in the result property. That array of NSManagedObjectID instances can then be used to update one or more NSManagedObjectContext instances.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Book.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs

do {
let context = CoreDataManager.shared.container.viewContext
let result = try context.execute(
deleteRequest
)

guard
let deleteResult = result as? NSBatchDeleteResult,
let ids = deleteResult.result as? [NSManagedObjectID]
else { return }

let changes = [NSDeletedObjectsKey: ids]
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: changes,
into: [context]
)
} catch {
print(error as Any)
}