How to use Core Data

Issue #785

Core Data

  • Responding to changes in a managed object context

    Calling mergeChanges on a managed object context will automatically refresh any managed objects that have changed. This ensures that your context always contains all the latest information. Note that you don’t have to call mergeChanges on a viewContext when you set its automaticallyMergesChangesFromParent property to true. In that case, Core Data will handle the merge on your behalf.

  • Using Core Data in the Background

    Managed objects retrieved from a context are bound to the same queue that the context is bound to.

  • The Laws of Core Data

    The ability to have a child MOC is neat in some corner cases. Let’s review the core functionality of MOCs in order to understand those cases:

  • MOCs load objects from its source (a PSC or another MOC)
  • MOCs save objects to its source (a PSC or another MOC)
  • MOCs enforce graph integrity when they save

Use Core Data as a local cache

When you save changes in a context, the changes are only committed “one store up.” If you save a child context, changes are pushed to its parent. Changes are not saved to the persistent store until the root context is saved. (A root managed object context is one whose parent context is nil.)

  • NSManagedObjectContext fetch

    An object that meets the criteria specified by request (it is an instance of the entity specified by the request, and it matches the request’s predicate if there is one) and that has been inserted into a context but which is not yet saved to a persistent store, is retrieved if the fetch request is executed on that context.

  • NSBatchDeleteRequest mergeChanges

    Changes are not reflected in the context”. So what you’re seeing is normal. Batch updates work directly on the persistent store file instead of going through the managed object context, so the context doesn’t know about them. When you delete the objects by fetching and then deleting, you’re working through the context, so it knows about the changes you’re making (in fact it’s performing those changes for you).

  • Does modifying a managed object in a parent context propagate down to child contexts when performing a child fetch in Core Data?

mainContext would fetch all the way to the persistent store. Fetches and objectWithID: only go as many levels as they need to.
It’s important to remember that when a context is created it’s a “snapshot” of the state of it’s parent. Subsequent changes to the parent will not be visible in the child unless the child is somehow invalidated

CloudKit

NSFetchedResultsController

How to fetch with background context

TLDR: Use NSFetchedResultsController in backgroundContext

  • If use Core Data as cache: convert to struct
  • If use NSManagedObject: use FetchRequest on viewContext

Approach 1: Treat Core Data as cache

Pros

  • backgroundContext.automaticallyMergesChangesFromParent
  • Use 1 shared background context to load and updates changes
  • Convert NSManagedObject to struct
  • Our struct can have additional logic and properties
  • Does not need to care about faulting

Cons

  • Need to update objectId When context.save, NSManagedObject ‘s objecteId is updated
  • Need to reload when underlying NSManagedObject changes
  • Need to handle relationship

Instead of plain NSFetchRequest, can use NSFetchedResultsController on background context. In its controllerDidChangeContent convert to struct

Approach 2: viewContext to persistent coordinator

1
viewContext -> persistent coordinator <- backgroundContext

Pros

  • viewContext.automaticallyMergesChangesFromParent
  • viewContext and background Context stems from persistent container
  • background context changes object
  • viewContext read only

Cons

  • viewContext blocks main thread

Approach 3: viewContext to background context

1
viewContext -> backgroundContext -> persistent coordinator

Pros

  • viewContext.automaticallyMergesChangesFromParent
  • viewContext reads from backgroundContext, no main queue block
  • backgroundContext changes

Cons

  • ???

Read more

Approach 4: background context -> mainContext -> backgroundContext -> persistent coordinator

https://stackoverflow.com/questions/35961936/child-nsmanagedobjectcontext-update-from-parent

1
2
3
RootContext (private queue) - saves to persistent store
MainContext (main queue) child of RootContext - use for UI (FRC)
WorkerContext (private queue) - child of MainContext - use for updates & inserts

Updated at 2021-02-28 07:58:23

How to convert struct to Core Data NSManagedObject

Issue #706

Use Mirror and set key value as NSManagedObject subclasses from NSObject

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
import CoreData

final class ManagedObjectConverter {
func convert<M>(m: M, context: NSManagedObjectContext) throws -> NSManagedObject {
let entityName = String(describing: m)
guard let entityDescription = NSEntityDescription.entity(
forEntityName: entityName,
in: context
) else {
throw AppError.parsing
}

let managedObject = NSManagedObject(
entity: entityDescription,
insertInto: context
)

let mirror = Mirror(reflecting: m)

guard mirror.displayStyle == .struct else {
throw AppError.parsing
}

for case let (label?, anyValue) in mirror.children {
managedObject.setValue(anyValue, forKey: label)
}

return managedObject
}
}

Updated at 2020-12-07 06:05:04

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


Updated at 2020-10-25 20:58:07

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)
}