Accessing NSManagedObject array crashes generates Assertion - ios

I am reading bunch of objects from core data store , when I am trying to read object from this array in later part of code Xcode asserts for wrong thread access on that statement. I have added -com.apple.CoreData.ConcurrencyDebug 1 flag to the scheme to find out issues related to core data concurrency . When I remove this flag everything works fine.
Here is how I read the data from core data store
let managedContext =
appDelegate.persistentContainer.newBackgroundContext()
//setup request to load persons ( cached contacts)
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Person")
fetchRequest.fetchBatchSize = 20
//read entties
managedContext.performAndWait {
do {
self.people = try managedContext.fetch(fetchRequest)
print("\(self.people.count) contacts retrieved from cache(CD)" )
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
And I am reading the array from other part of code as below
let tempContact = self.people[index]
var property = tempContact.value(forKey: "firstName") as! String
I get assertion on second line above. The above code resides inside a function which I have tried calling as below
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let moc = appDelegate.persistentContainer.viewContext
let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateMOC.parent = moc
privateMOC.perform {
self.updateChangedContacts()
}
self.contactsTableView.reloadData()
If I simply call updateChangedContacts directly it works , but if I have large number of contacts this operation is going to take significant time blocking main UI.
I want to avoid that , how can i approach this ?

Related

Core Data operations on multiple thread causing insertion but no fetching

I am using CoreData to insert data and fetch Data, as I have a lot of data, so I am using core data on multiple threads in order to be thread-safe.
The problem is that I am able to insert Data in CoreData, but while fetching from CoreData, the results are zero, this is happening when I kill my app and fetch the data from Database. This has something to do with NSMangedObjectContext but I am not able to figure it out.
Here is my code snippet :
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "My_Contacts")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext() {
let context = CoreDataManager.sharedManager.persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func insertContact(id:Int, firstName : String,lastName : String,emaild : String,isFavorite : Bool,phoneNum : String,profilePic : String,sync : Bool) -> Contact? {
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext
let privateManagedObjectContext: NSManagedObjectContext = {
//NSPrivateQueueConcurrencyType
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.parent = managedContext
return moc
}()
let entity = NSEntityDescription.entity(forEntityName: "Contact",
in: privateManagedObjectContext)!
let contact = NSManagedObject(entity: entity,
insertInto: privateManagedObjectContext)
contact.setValue(firstName, forKey: "first_name")
contact.setValue(lastName, forKey: "last_name")
contact.setValue(emaild, forKey: "email")
contact.setValue(isFavorite, forKey: "favorite")
contact.setValue(phoneNum, forKey: "phone_number")
contact.setValue(profilePic, forKey: "profile_pic")
contact.setValue(sync, forKey: "syncStatus")
contact.setValue(id, forKey: "contactId")
do {
try privateManagedObjectContext.save()
return contact as? Contact
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
return nil
}
}
func fetchAllContacts() -> [Contact]?{
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Contact")
do {
let people = try managedContext.fetch(fetchRequest)
return people as? [Contact]
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return nil
}
}
}
so I am using core data on multiple threads in order to be thread-safe
What do you mean by this? Using multiple threads doesn't make anything thread-safe. Thread safety relates to your ability to run your code on multiple threads without problems, and it generally requires that you take a number of precautions to prevent threads from interfering with each other.
The problem is that I am able to insert Data in CoreData, but while fetching from CoreData, the results are zero, this is happening when I kill my app and fetch the data from Database. This has something to do with NSMangedObjectContext but I am not able to figure it out.
You need to understand what a managed object context is. Think of it like a temporary workspace: you can execute a fetch request to bring objects from a persistent store into a managed object context, and you can add new objects to the context, and you can manipulate the objects in a context. The changes you make in a context don't mean anything outside the context until you save the context back to the persistent store.
A few reasons you might not be seeing the objects you're adding are:
You're adding the objects and trying to read them back in different contexts.
You never save the context after you add objects.
You save the context in which you added the object, but the parent context (managed object contexts are hierarchical) is never saved.
You try to save the context after you add objects, but saving fails.
You're using the same context in multiple threads without taking care to serialize the operations on the context (which is to say, your code isn't thread-safe).
What you really should do to figure this out is to get yourself back to a state where you can store and retrieve objects reliably. Try using just one thread and make sure that your operations work. If they don't, fix that first. Next, get a solid understanding of how managed object contexts work and how to use them. Finally, read up on concurrency and Core Data.
Since you are using multiple MOC(Managed Object Context), you need to save both the contexts
You have set privateManagedObjectContext's parent to managedContext, but you are not saving managedContext
After calling privateManagedObjectContext.save(), you need to call managedContext.save() as well

Why entries are not deleted until app is restarted or I execute my NSBatchDeleteRequest twice?

I'm reading Delete/Reset all entries in Core Data?.
If I follow the steps below, I get an unexpected result:
Call the code below
Then query an entity in the simulator, I will get an entity back!!!
If I call clearCoreDataStore again (or do a restart), only then the value won't be retrieved from core-data
What am I missing?
func clearCoreDataStore() {
let entities = dataManager.persistentContainer.managedObjectModel.entities
for entity in entities {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity.name!)
let deleteReqest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try context.execute(deleteReqest)
} catch {
print(error)
}
}
dataManager.saveContext()
}
The objects being deleted from the persistent store are probably also in an in-memory object context. If so, that memory context must first be updated to reflect the deletions. A thorough discussion can be found here.
In a nutshell...
deleteRequest.resultType = NSBatchDeleteRequestResultType.resultTypeObjectIDs
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSDeletedObjectsKey : objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])

App stuck till save data into core data

I am new to core data in iOS and i have got around 5000 record with multiple key value .
I want to save it in core data storage in backgorund so app will not stuck. I have saved it already and the code is implemented in Appdelegate.swift class but when app run its stuck till the data load from the API and save into core data storage.
Here is my Code :-
let tempArray = NSMutableArray(array : data!)
for i in 0 ..< tempArray.count {
print(i)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
if #available(iOS 10.0, *) {
self.moc = appDelegate.persistentContainer.viewContext
}
let entity = NSEntityDescription.insertNewObject(forEntityName: "Product", into: self.moc!) as! Product
if (tempArray.object(at: i) as! NSDictionary).value(forKey: "id") != nil {
entity.setValue((tempArray.object(at: i) as! NSDictionary).value(forKey: "id") as! NSNumber , forKey: "id")
}
DispatchQueue.main.async {
do {
try self.moc?.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
i have tried with try self.moc?.save() to put in dispatch async but its giving error
You can do it with OperationQueue
let op = OperationQueue()
op.addOperation {
//Your task goes here
do {
try self.moc.save()
}
catch {
fatalError("Failure to save context: \(error)")
}
}
method 2
DispatchQueue.main.async {
do {
self.moc.save()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
Do like this
moc.perform {
if let a = (tempArray.object(at: i) as! NSDictionary).value(forKey: "id") {
let obj = NSManagedObject(entity: entity, insertInto: moc)
obj.setValue(a as! NSNumber , forKey: "id")
}
}
DispatchQueue.main.async {
do {
try self.moc.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
It appears that you are using a single main-thread context. ManagedObjectContexts are not thread safe so they must be run on the thread they were created on. Main-thread managedObjectContext are great for reading - because you generally display your data on the main thread - but they are terrible for writing because you would rather that done in the background. Queues or dispatch async won't fix it, because the context must be executed on the main thread.
Having a single context for the application only works if your app is super small and simple. In the past there were many setups on how to have a core-data setup with background saving and have the main context for displaying data. Since iOS 10 there is NSPersistentContainer which is a automatic setup for a great core-data stack which solves all theses issues.
If you are only supporting iOS 10+ then I strong recommend using NSPersistentContainer. This is how you use it: Treat the viewContext as readonly and never write data on the main thread. Only write data using performBackgroundTask: using the context that is given to your block of code. Don't pass managedObject into or out of the these blocks. If you need to modify a managedObject on the viewContext save its objectID and refetch in the background task. Also in you core-data setup set the (viewContext to automatically merge changes self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = YES;)

How to use core data in background ?

After creating simple code to getting data from CoreData, I realised that some unknown errors are fired. I read that fetching data in background is not that simple. Unfortunately I'm new in iOS developing and only know Swift, where there are many objective-C tutorials for that.
This is my code:
func reloadData() {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Person")
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
let people = results as! [NSManagedObject]
for person in people {
collectionViewData.append(person)
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView?.reloadData()
}
}
}
How to implement this with NSPrivateQueueConcurrencyType and stuff like that in Swift?
Your code will almost certainly fail. You are accessing your main managed object context from a background thread.
From your code it seems that you do not need a background fetch. Remove all GCD (grand central dispatch) parts of your code and you should be fine.
For reference, this is how you do a background operation in Core Data:
let moc = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
moc.parentContext = <insert main context here>
moc.performBlock {
// make changes, fetch, save
}
After saving, your main context will have the changes. To update a table view, implement NSFetchedResultsController and its delegate methods to do it automatically.

Deleting items in Core Data

Before deleteData() method was called, I've verified that there are two instances in my Core Data. Now I need to delete one away. I referred to this tutorial to delete my core data.
I managed to copy the first NSManagedObject instance through let person = people[0] as! NSManagedObject through this code, but app terminated on this line managedContext.delete(person).
The error faced was Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSManagedObjectContext delete:]: unrecognized selector sent to instance 0x7fefe28092e0'
func deleteData() {
var people = [Particulars]()
let fetchRequest = NSFetchRequest(entityName: "Particulars")
let appDel = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDel.managedObjectContext
do {
people = try managedContext.executeFetchRequest(fetchRequest) as! [Particulars]
do {
let person = people[0] as! NSManagedObject
managedContext.delete(person)
try managedContext.save()
} catch let error as NSError {
print("Could not delete data \(error)")
}
} catch let error as NSError
{
print("Could not fetch data \(error)")
}
}
The proper call is not delete, but deleteObject for NSManagedObjectContext.
Also note that it is more readable to write person.first rather than people[0], but it is of course a matter of taste.
I think let person = people[0] as! NSManagedObject is problematic, because
It is not safe and would crash if there are no search results. Use if let person = people.first...
The downcast from Particulars to NSManagedObject also does not make sense. You have already performed a cast when getting the search results.

Resources