I'm writing an application using the MVVM pattern. And I'm wondering know how to create the CoreData stack so it can be accessed from various places in my app.
First approach is to create a persistent container in the AppDelegate and then inject this service to my ViewModels (simultaneously passing the managedObjectContext as an environment variable to my Views).
This way, however, accessing context throughout the app is more difficult: e.g. in decoding network responses, as they don't have access to the managedObjectContext:
protocol APIResource {
associatedtype Response: Decodable
...
}
extension APIResource {
func decode(_ data: Data) -> AnyPublisher<Response, APIError> {
Just(data)
// how can I access context here to pass it to JSONDecoder?
.decode(type: Response.self, decoder: JSONDecoder())
.mapError { error in
.parsing(description: error.localizedDescription)
}
.eraseToAnyPublisher()
}
}
The other solution I've seen is to use a singleton. I can access it from anywhere in the project but how can I create it in the right way?
What if I wan't to modify some object in the main and the background queue at the same time? Or what if both queues want to modify the same object?
You can use Core Data Singleton class
import CoreData
class CoreDataStack {
static let shared = CoreDataStack()
private init() {}
var managedObjectContext: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
var workingContext: NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.managedObjectContext
return context
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyStuff")
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
RaiseError.raise()
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext() {
self.managedObjectContext.performAndWait {
if self.managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
appPrint("Main context saved")
} catch {
appPrint(error)
RaiseError.raise()
}
}
}
}
func saveWorkingContext(context: NSManagedObjectContext) {
do {
try context.save()
appPrint("Working context saved")
saveContext()
} catch (let error) {
appPrint(error)
RaiseError.raise()
}
}
}
Core Data is not thread safe. If you write something on manageObject and don't want to save that, but some other thread save the context, then the changes that you don't want to persist will also persist.
So to avoid this situation always create working context - which is private.
When you press save, then first private context get saved and after that you save main context.
In MVVM you should have DataLayer through which your ViewModel interact with Core Data singleton class.
Related
I want to perform a background fetch and pass the result to closure. Currently I'm using performBackgroundTask method from NSPersistentContainer which is giving a NSManagedObjectContext as a closure. Then using that context I'm executing fetch request. When fetch is done I'm, passing the result to the completion handler.
func getAllWorkspacesAsync(completion: #escaping ([Workspace]) -> Void) {
CoreDataStack.shared.databaseContainer.performBackgroundTask { childContext in
let workspacesFetchRequest: NSFetchRequest<Workspace> = NSFetchRequest(entityName: "Workspace")
workspacesFetchRequest.predicate = NSPredicate(format: "remoteId == %#", "\(UserDefaults.lastSelectedWorkspaceId))")
do {
let workspaces: [Workspace] = try childContext.fetch(workspacesFetchRequest)
completion(workspaces)
} catch let error as NSError {
// Handle error
}
}
}
I'm going to call this method from ViewModel and use Combine PassthroughSubject to notify the ViewController about the event.
class WorkspaceViewModel {
private var cancellables: Set<AnyCancellable> = []
let resultPassthroughObject: PassthroughSubject<[Workspace], Error> = PassthroughSubject()
private let cacheManager = WorkspaceCacheProvider.shared
static public let shared: WorkspaceViewModel = {
let instance = WorkspaceViewModel()
return instance
}()
func fetchWorkspaces() {
cacheManager.getAllWorkspacesAsync { [weak self] workspaces in
guard let self = self else { return }
self.resultPassthroughObject.send(workspaces)
}
}
}
And the ViewController code:
class WorkspaceViewController: UIViewController {
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
WorkspaceViewModel.shared.resultPassthroughObject
.receive(on: RunLoop.main)
.sink { _ in } receiveValue: { workspaces in
// update the UI
}
.store(in: &cancellables)
}
}
My question is: is it safe to pass NSManagedObject items?
No, it is not
Do not pass NSManagedObject instances between queues. Doing so can result in corruption of the data and termination of the app. When it is necessary to hand off a managed object reference from one queue to another, use NSManagedObjectID instances.
You would want something like:
func getAllWorkspacesAsync(completion: #escaping ([NSManagedObjectID]) -> Void) {
CoreDataStack.shared.databaseContainer.performBackgroundTask { childContext in
let workspacesFetchRequest: NSFetchRequest<Workspace> = NSFetchRequest(entityName: "Workspace")
workspacesFetchRequest.predicate = NSPredicate(format: "remoteId == %#", "\(UserDefaults.lastSelectedWorkspaceId))")
do {
let workspaces: [Workspace] = try childContext.fetch(workspacesFetchRequest)
completion(workspaces.map { $0.objectID } )
} catch let error as NSError {
// Handle error
}
}
}
You will need to make corresponding changes to your publisher so that it publishes [NSManagedObjectID].
Once you have received this array in your view controller you will need to call object(with:) on your view context to get the Workspace itself. This will perform a fetch in the view context for the object anyway.
You may want to consider whether the time taken to fetch your workspaces warrants the use of a background context. By default Core Data objects are retrieved as faults and the actual attribute values are only fetched when the fault is triggered by accessing an object's attributes.
Alternatively, if the purpose of the view controller is to present a list of workspaces that the user can select from, you could return an array of tuples or structs containing the workspace name and the object id. However, this all sounds like pre-optimisation to me.
I have CoreDataStack
I've added debug options for CoreData debugging
"-com.apple.CoreData.ConcurrencyDebug 1"
class CoreDataStack {
public enum SaveStatus {
case saved, rolledBack, hasNoChanges, error
}
private var modelName: String
var viewContext: NSManagedObjectContext
var privateContext: NSManagedObjectContext
var persisterContainer: NSPersistentContainer
init(_ modelName: String) {
self.modelName = modelName
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores { persisterStoreDescription, error in
print("CoreData", "Initiated \(persisterStoreDescription)")
guard error == nil else {
print("CoreData", "Unresolved error \(error!)")
return
}
}
self.persisterContainer = container
self.viewContext = container.viewContext
self.viewContext.automaticallyMergesChangesFromParent = true
self.privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
self.privateContext.persistentStoreCoordinator = container.persistentStoreCoordinator
self.privateContext.automaticallyMergesChangesFromParent = true
}
func createTemporaryViewContext() -> NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = self.privateContext
context.automaticallyMergesChangesFromParent = true
return context
}
func saveTempViewContext(tempContext context: NSManagedObjectContext, completion: ((CoreDataStack.SaveStatus) -> Void)? = nil) {
guard context.hasChanges || privateContext.hasChanges else {
completion?(.hasNoChanges)
return
}
context.performAndWait {
do {
try context.save()
}
catch {
completion?(.error)
return
}
}
privateContext.perform { [weak self] in
do {
try self?.privateContext.save()
completion?(.saved)
}
catch {
self?.privateContext.rollback()
completion?(.rolledBack)
}
}
}
class ViewController: UIViewController {
#objc
func save(_ sender: UIBarButtonItem) {
let coreDataStack = CoreDataStack()
let tempMainContext = coreDataStack.createTemporaryViewContext() //child main context from private context
var people = People(context: tempMainContext)
self.people.name = "John Doe"
self.people.age = 25
coreDataStack.saveTempViewContext(tempContext: tempMainContext) { status in
print(status)
}
}
}
I have "privateContext" attached to coordinator
I've created "tempMainContext" from private context
When I call "saveTempViewContext" I want to save tempMainContext which pushes changes to parent (privateContext) and this privateContext saves to persistent store
So the error occurred in line
privateContext.hasChanges
I know thats this line executes in main thread. And I need to call method "perform" or "performAndWait" to perform on the right queue.
like this
var contextHasChanges: Bool = false
var privateContextHasChanges: Bool = false
context.performAndWait {
contextHasChanges = context.hasChanges
}
privateContext.performAndWait {
privateContextHasChanges = privateContext.hasChanges
}
guard context.hasChanges || privateContext.hasChanges else {
completion?(.hasNoChanges)
return
}
But it so weird to call "performAndWait" just to check that context has changes. And when I call "performAndWait" it block current thread in my case it MainThread. And I don't want block the main thread even for short time.
How could we resolve this issue ?
UPD 1
attached debug stack
UPD 2
In my CoreDataStack init method in below I'v added code.
I just check if the private context has changes and It will crash
let privateContextHasChanges = privateContext.hasChanges
I think it's because at that line we are in MainThread and we touch private context which init with "privateQueueConcurrencyType" and I investigate that if I touch other property for example "privateContext.name" or "privateContext.parent" it works fine.
But if I touch property like this:
privateContext.hasChanges
privateContext.registeredObjects
privateContext.updatedObjects
maybe other
it will crash again
So I can make a conclusion that these properties are not thread safe.
Can anyone confirm that ?
UPD 3
After I'v read post Unexpected Core Data Multithreading Violation
I'v made conclusions:
If I'm on the main thread and my context's type is .main I do not need any changes and I safe
If I'm on some place and I don't know want kind of thread I'm on I always need to do "perform" or "performAndWait" to synchonize queue attached to context.
Almost always do "perform" and not "performAndWait" except you can't do without it.
context.hasChanges isn'n thread safe. It requires to be used in the context thread.
https://developer.apple.com/forums/thread/133378
I am facing this issue once or twice a day for the last week when I open my app & the app tries any save operation on the context, I still can't find a way to reproduce it.
I have searched many question on SO for fix, but most of them point 2 issues
Core Data Migration issue(which I don't have as I am on the same Model version no.)
failure of loading the persistent store (which is also doesn't happen in my case as my Core Data Stack doesn't initialise the main UI if the loadPersistentStores method on the persistentContainer fails)
I am using the Core Data stack setup mentioned in the below link:
https://williamboles.me/progressive-core-data-migration/
Here is my CoreData Setup class:
lazy var persistentContainer: NSPersistentContainer = {
let persistentContainer = NSPersistentContainer(name: "ABC")
let description = persistentContainer.persistentStoreDescriptions.first
description?.shouldInferMappingModelAutomatically = false //inferred mapping will be handled else where
description?.shouldMigrateStoreAutomatically = false
description?.type = storeType
return persistentContainer
}()
lazy var managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var _managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
return context
}()
// MARK: - Singleton
private static var privateShared : CoreDataManager?
class func shared() -> CoreDataManager { // change class to final to prevent override
guard let uwShared = privateShared else {
privateShared = CoreDataManager()
return privateShared!
}
return uwShared
}
class func destroy() {
privateShared = nil
}
// MARK: - Init
init(storeType: String = NSSQLiteStoreType, migrator: CoreDataMigratorProtocol = CoreDataMigrator()) {
self.storeType = storeType
self.migrator = migrator
}
// MARK: - SetUp
func setup(completion: #escaping () -> Void) {
loadPersistentStore {
completion()
}
}
// MARK: - Loading
private func loadPersistentStore(completion: #escaping () -> Void) {
migrateStoreIfNeeded {
self.persistentContainer.loadPersistentStores { description, error in
guard error == nil else {
fatalError("was unable to load store \(error!)")
}
completion()
}
}
}
private func migrateStoreIfNeeded(completion: #escaping () -> Void) {
guard let storeURL = persistentContainer.persistentStoreDescriptions.first?.url else {
fatalError("persistentContainer was not set up properly")
}
if migrator.requiresMigration(at: storeURL, toVersion: CoreDataMigrationVersion.current) {
DispatchQueue.global(qos: .userInitiated).async {
self.migrator.migrateStore(at: storeURL, toVersion: CoreDataMigrationVersion.current)
DispatchQueue.main.async {
completion()
}
}
} else {
completion()
}
}
And I initialise the Core Data stack using the following code in the App Delegate:
CoreDataManager.shared().setup {[unowned self] in
self.showMainUI()
}
My App crashes after the Home Controller is loaded & some part of my code does a save operation on certain NSManagedObject Model
This how I save to Context:
let context = CoreDataManager.shared().managedObjectContext // background context
context.performAndWait {
if let entityDescription = NSEntityDescription.entity(forEntityName: Entity_Name, in: context) {
if let runEntityObject = NSManagedObject(entity: entityDescription, insertInto: context) as? MY_Model {
// Create the Object
guard context.hasChanges else { return }
do {
try context.save() // Crashes here once or twice a day :(
}
catch {
print(error.localizedDescription)
}
}
}
}
Some SO answers also mention of threading issues but I am using the performAndWait Block so the save happen on the same queue
Would be really helpful If someone pointed me in the right direction regarding this issue
After going through my AppDelegate file many times, I found that I was doing a Core Data save operation in the applicationDidBecomeActive method which is also called when the app starts from a suspended state.
So if my Core Data stack setup closure didn't finish before the applicationDidBecomeActive is called the app would crash.
After removing it, the app was working fine without any crashes
I'm recently having a big headache in my app:
In my application, each of the units has almost 100 attributes to keep, and some of them may be updated very frequently, even tens or more updates per unit in a few second during initial synchronisation. As putting them all together in one table can be very inefficient, and there are some of them which are more likely to be resynchronized, I tried to break the unit to 5 - 6 smaller tables according to the functionality and possible update frequency, each of them has 8 - 32 fields and one more for primary key. To prevent main thread from blocking, I made an background context to update the data as well. Whenever context.save() is called, the main context is merged with the background context.
However when I try to do the batch testing, let's say about 50 units in the table, I find that the UI is still seriously blocked because the main context (which is responsible for fetching UI layout according to units attributes in the database) is busying at merging with intensive background context update.
I also found that there are more 10000 merging when reloading data even if there are only 50 units, I looked into it and found that whenever one context.save() is called, my app gets up to 9 NSManagedObjectContextDidSave notifications, which means most of the merging are actually redundant.
I'm also wondering if there is better way (in both context management or data table normalization design's perspective) to deal with the database structure problem: while the amount of records is not huge (probably less than 1000 in total), but have quite a lot of fields to maintain. The technique for manipulating huge records are already mature enough to improve efficiency, but as for dealing with huge fields, I can find very limited resources, not to mention resources in CoreData.
Not sure if changing to periodically context saving is a good idea because it's a bluetooth application and data can come and go at any time, any thread, thus the accuracy of instant data in local DB is critical on taking the right action. However, the changes won't be taken if I don't commit it via context.save() right away.
To sum up, there are two questions here:
Why there are multiple NSManagedObjectContextDidSave triggered while context.save() are just called once?
While dealing with this kind of database structure (with lots of fields per record), is there any better model which can fulfill a better trade-off between UI experience and data accuracy?
By the way, To improve UI performance, I do use NSFetchedResultsController but find that it can do very little if my tables have relationships, so what I do is to put the most significant attributes in the main entity which NSFetchedResultsController fetches, however this doesn't help much as the main context is still busying merging.
My code on manipulating core data are written as follow:
// MARK: - Example on saving a NSManagedObject in background thread
class ExampleRepository : CoreDataStack {
func updateRecordToDbInBackground(uuid: String, values: [String: Data]) {
let ctx = getBackgroundContext()
ctx.perform {
guard let resultId = self.fetchByUuid(uuid: uuid, context: ctx) else {
self.addRecordToDb(uuid: uuid, values: values, context: ctx)
self.saveContext(context: ctx, method: #function)
return
}
let result = ctx.object(with: resultId)
_ = self.updateRecordToDb(uuid: uuid, values: values, managedObject: result, context: ctx)
self.saveContext(context: ctx, method: #function)
}
}
func fetchByUuid(uuid: String, context: NSManagedObjectContext) -> NSManagedObjectID? {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: ENTITY_NAME)
fetchRequest.predicate = NSPredicate(format: "SELF.uuid = %#", uuid)
do {
return try context.fetch(fetchRequest).first?.objectID
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
return nil
}
func updateRecordToDb(uuid: String, values: [String: Data], managedObject: NSManagedObject?, context: NSManagedObjectContext) -> NSManagedObject {
let registerObj = managedObject ?? getManagedObject(context: context)
registerObj.setValue(uuid, forKey: FIELD_NAME_UUID)
let convertedValues: NSMutableDictionary = [:]
values.forEach { e in
convertedValues.addEntries(from: e.key: e.value])
}
registerObj.setValuesForKeys(convertedValues as! [String: Data])
return registerObj
}
func addRecordToDb(uuid: String, values: [String: Data], managedObject: NSManagedObject?, context: NSManagedObjectContext) -> NSManagedObject {
// add is generally similar to updateRecoredToDb, but adding some relationships to other entities
}
}
// MARK: - Core data stack
class CoreDataStack {
init(){
NotificationCenter.default.addObserver(self, selector: #selector(self.contextDidSaveContext), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil)
}
deinit{
NotificationCenter.default.removeObserver(self)
}
var _context: NSManagedObjectContext {
return SingletonContext.sharedInstance._context
}
var _backgroundContext: NSManagedObjectContext {
return SingletonContext.sharedInstance._backgroundContext
}
func saveContext(context: NSManagedObjectContext, method: String) {
guard context.hasChanges else {
return
}
context.perform {
do {
try context.save()
print("Context called from \(method) saved!")
} catch let error as NSError {
print("Could not save context called from \(method), Error: \(error), \(error.userInfo)")
}
}
}
#objc func contextDidSaveContext(notification: NSNotification) {
// The question is here: This func is called nine times when the context.save() were called once.
let sender = notification.object as! NSManagedObjectContext
if sender === self._context {
print("******** Merge background context with main ********")
self._backgroundContext.perform {
self._backgroundContext.mergeChanges(fromContextDidSave: notification as Notification)
}
}
else if sender === self._backgroundContext {
print("******** main main context with background ********")
self._context.perform {
self._context.mergeChanges(fromContextDidSave: notification as Notification)
}
}
}
}
class SingletonContext {
static let sharedInstance = SingletonContext()
var _persistentStoreCoordinator: NSPersistentStoreCoordinator {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.persistentStoreCoordinator
}
lazy var _context: NSManagedObjectContext = {
let ctx = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType)
ctx.persistentStoreCoordinator = self._persistentStoreCoordinator
return ctx
}()
lazy var _backgroundContext: NSManagedObjectContext = {
let ctx = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType)
ctx.persistentStoreCoordinator = self._context.persistentStoreCoordinator
return ctx
}()
}
I try to avoid crash when many contexts save at same time.
The following class has one operation queue that operate only one work at same time. It has three context. First, defaultContext is main queue type, this is not directly updated and is only visible to the user. Other two contexts is localContext and externalContext.
LocalContext is for user's schedule addition and external Context is for external schedule update like cloud sync. Local context and external context is child of defaultContext and set it's automaticallyMergesChangesFromParent property to true. Even if user update and external update are implemented at same time. Since they run sequentially in the same queue, there is no data loss.
It works great when data input is small. But app gets slower when too much data is coming in. Is there any better way?
Here's my code.
class DataController {
static let shared = DataController()
var schedules: [Schedule] = []
var persistentContainer: NSPersistentContainer
let persistentContainerQueue = OperationQueue()
private init() {
persistentContainerQueue.maxConcurrentOperationCount = 1
persistentContainer = NSPersistentContainer(name: "CoreDataConcurrency")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
lazy var defaultContext: NSManagedObjectContext = {
[unowned self] in
self.persistentContainer.viewContext
}()
lazy var localContext: NSManagedObjectContext = {
[unowned self] in
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.defaultContext
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var externalContext: NSManagedObjectContext = {
[unowned self] in
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.defaultContext
context.automaticallyMergesChangesFromParent = true
return context
}()
func enqueueCoreDataOperation(context: NSManagedObjectContext, changeBlock: #escaping () -> (NSManagedObjectContext)) {
persistentContainerQueue.addOperation {
let changedContext = changeBlock()
guard changedContext.hasChanges else {
return
}
changedContext.performAndWait({
do {
try changedContext.save()
if let parentContext = changedContext.parent {
do {
try parentContext.save()
} catch {
fatalError()
}
}
} catch {
fatalError()
}
})
}
}
func addSchedule(title: String, date: Date, context: NSManagedObjectContext) {
let changeBlock: () -> (NSManagedObjectContext) = {
let schedule = NSEntityDescription.insertNewObject(forEntityName: "Schedule", into: context) as! Schedule
schedule.title = title
schedule.date = date
return context
}
enqueueCoreDataOperation(context: context, changeBlock: changeBlock)
}
func updateSchedule(schedule: Schedule, modifiedTitle: String, context: NSManagedObjectContext) {
let scheduleInContext = context.object(with: schedule.objectID) as! Schedule
let changeBlock: () -> (NSManagedObjectContext) = {
scheduleInContext.title = modifiedTitle
return context
}
enqueueCoreDataOperation(context: context, changeBlock: changeBlock)
}
}
You could batch up the incoming data into smaller batches so each operation takes less time and add a priority to the operations so cloud based changes have a lower priority. Then they will no longer block the other changes. But I strongly suspect that you are doing something wrong in you import operation that is making take too long. Are you doing a fetch for each imported entity? Please share that code.