CoreData Concurrency issue - ios

I am having issue while using private managedObjectContextfor saving data in background. I am new to CoreData. I am using Parent-Child approach for NSManagedObjectContext but facing several issues.
Errors arise when I tap reload button multiple times
Errors:
'NSGenericException', reason: Collection <__NSCFSet: 0x16e47100> was mutated while being enumerated
Some times : crash here try managedObjectContext.save()
Sometimes Key value coding Compliant error
My ViewController class
class ViewController: UIViewController {
var jsonObj:NSDictionary?
var values = [AnyObject]()
#IBOutlet weak var tableView:UITableView!
override func viewDidLoad() {
super.viewDidLoad()
getData()
saveInBD()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.saved(_:)), name: "kContextSavedNotification", object: nil)
}
//Loding json data from a json file
func getData(){
if let path = NSBundle.mainBundle().pathForResource("countries", ofType: "json") {
do {
let data = try NSData(contentsOfURL: NSURL(fileURLWithPath: path), options: NSDataReadingOptions.DataReadingMappedIfSafe)
do {
jsonObj = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
} catch {
jsonObj = nil;
}
} catch let error as NSError {
print(error.localizedDescription)
}
} else {
print("Invalid filename/path.")
}
}
**Notification reciever**
func saved(not:NSNotification){
dispatch_async(dispatch_get_main_queue()) {
if let data = DatabaseManager.sharedInstance.getAllNews(){
self.values = data
print(data.count)
self.tableView.reloadData()
}
}
}
func saveInBD(){
if jsonObj != nil {
guard let nameArray = jsonObj?["data#"] as? NSArray else{return}
DatabaseManager.sharedInstance.addNewsInBackGround(nameArray)
}
}
//UIButton for re-saving data again
#IBAction func reloadAxn(sender: UIButton) {
saveInBD()
}
}
**Database Manager Class**
public class DatabaseManager{
static let sharedInstance = DatabaseManager()
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
private init() {
}
func addNewsInBackGround(arr:NSArray) {
let jsonArray = arr
let moc = managedObjectContext
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc
privateMOC.performBlock {
for jsonObject in jsonArray {
let entity = NSEntityDescription.entityForName("Country",
inManagedObjectContext:privateMOC)
let managedObject = NSManagedObject(entity: entity!,
insertIntoManagedObjectContext: privateMOC) as! Country
managedObject.name = jsonObject.objectForKey("name")as? String
}
do {
try privateMOC.save()
self.saveMainContext()
NSNotificationCenter.defaultCenter().postNotificationName("kContextSavedNotification", object: nil)
} catch {
fatalError("Failure to save context: \(error)")
}
}
}
func getAllNews()->([AnyObject]?){
let fetchRequest = NSFetchRequest(entityName: "Country")
fetchRequest.resultType = NSFetchRequestResultType.DictionaryResultType
do {
let results =
try managedObjectContext.executeFetchRequest(fetchRequest)
results as? [NSDictionary]
if results.count > 0
{
return results
}else
{
return nil
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
return nil
}
}
func saveMainContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
print("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

You can write in background and read in the main thread (using different MOCs like you do). And actually you're almost doing it right.
The app crashes on the try managedObjectContext.save() line, because saveMainContext is called from within the private MOC's performBlock. The easiest way to fix it is to wrap the save operation into another performBlock:
func saveMainContext () {
managedObjectContext.performBlock {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
print("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Other two errors are a little more tricky. Please, provide more info. What object is not key-value compliant for what key? It's most likely a JSON parsing issue.
The first error ("mutated while being enumerated") is actually a nasty one. The description is pretty straight forward: a collection was mutated by one thread while it was enumerated on the other. Where does it occur?
One possible reason (most likely one, I would say) is that it is indeed a Core Data multithreading issue. Despite the fact that you can use several threads, you can only use core data objects within the thread they were obtained on. If you pass them to another thread, you'll likely run into an error like this.
Look through your code and try to find a place where such situation might occur (for instance, do you access self.values from other classes?). Unfortunately, I wasn't able to find such place in several minutes. If you said upon which collection enumeration this error occurs, it would help).
UPDATE:
P.S. I just thought that the error might be related to the saveMainContext function. It is performed right before a call to saved. saveMainContext is performed on the background thread (in the original code, I mean), and saved is performed on the main thread. So after fixing saveMainContext, the error might go away (I'm not 100% sure, though).

You are violating thread confinement.
You cannot write to CoreData in Background, and read in MainThread.
All operation on CoreData must be done in the same thread

Related

Attempting to fetch and update a specific Core Data entity within master-detail view

I'm trying to fetch the specific instance of an item in a list but I can't recall the best way to do so. This is in the detail view of a master-detail list that when text changes end, it updates the item in the master list. However, I cant seem to get it to update the correct item, I know it lies in this part of my code:
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "Event")
do {
let test = try context.fetch(fetchRequest)
let objectUpdate = test[0] as! NSManagedObject
objectUpdate.setValue(noteText.text, forKey: "title")
do {
try context.save()
}
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
Specifically this part:
let test = try context.fetch(fetchRequest)
let objectUpdate = test[0] as! NSManagedObject
objectUpdate.setValue(noteText.text, forKey: "title")
EDIT:
I forgot to post the question! Basically with this current method, when I finish typing in the text field (this is wrapped in a textFieldDidEndEditing function) it saves, but at the lowest item in the master tableview. I want it to update the item that was selected.
Since I'm trying to grab the item from the tableview and update it, what is the best method for this? I'm assuming that I need to identify the correct item, but I'm not sure of the best method for this.
I ended up fixing it, I made a call for the coredata object as a variable, then passed it through with a segue. then all i needed to do was make selectedNote?.title = noteText.text
This is the full complete code:
var selectedEvent: Event? = nil
#IBAction func textFieldEndEdit(_ sender: Any) {
        print("edit end")
        
          guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
                    return
                }
                let context = appDelegate.persistentContainer.viewContext
                
                do {
                    
                    selectedEvent?.title = noteText.text
                    
                    do {
                        try context.save()
                    }
                    
                } catch {
                    let nserror = error as NSError
                    fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
                }
        
    }

how to validate login form into next page using Sqllite in iOS?

**this is to how to create a login validation form to move from login to next view controller **
**fetching the data from database**
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
**it stores the data**
let managedContext = appDelegate.persistentContainer.viewContext
//it fetches the data
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Details")
**validation code to check it but the condition fails**
do {
let result = try managedContext.fetch(fetchRequest)
for data in result as! [NSManagedObject] {
if ([emailid.text].count != 0 && [password.text].count != 0){
if (emailid.text == data.value(forKey: "emailId") as? String) && (password.text == data.value(forKey: "passWord") as? String){
let secondvc = storyboard?.instantiateViewController(withIdentifier: "loginVcID") as! loginVc
self.navigationController?.pushViewController(secondvc, animated: true)
}
in this condition it is not moving to next view controller
to check another condition
}
else {
self.label.text = "enter a valid data"
}
}
}
**when it fails it goes to catch to show that**
catch
{
print("Failed")
}
}
}
**this code is for registration to save into database**
**to create a database and store the value**
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let detailEntity = NSEntityDescription.entity(forEntityName: "Details", in: managedContext)!
** creation of database**
let detail = NSManagedObject(entity: detailEntity, insertInto: managedContext)
detail.setValue(username.text, forKeyPath: "userName")
detail.setValue(emailid.text, forKey: "emailId")
detail.setValue(password.text, forKey: "passWord")
detail.setValue(city.text, forKey: "city")
**saving the data**
do {
try managedContext.save()
}
** it display whatever in that method**
catch let error as NSError
{
its shows error when it fails
print("Could not save. (error), (error.userInfo)")
}
Go to you appDelegate, you will find a line // MARK: - Core Data stack & // MARK: - Core Data Saving support, remove the saveContext() function & also remove persistentContainer.. Then
Add this class to your project
final class PersistenceManager {
private init() {}
static let shared = PersistenceManager()
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "ProjectNAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var context = persistentContainer.viewContext
// MARK: - Core Data Saving support
func save() {
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func fetch<T: NSManagedObject>(_ objectType: T.Type) -> [T] {
let entityName = String(describing: objectType)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
do {
let fetchedObjects = try context.fetch(fetchRequest) as? [T]
return fetchedObjects ?? [T]()
} catch {
return [T]()
}
}
func deleteAll<T: NSManagedObject>(_ objectType: T.Type) {
let entityName = String(describing: objectType)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: context)
} catch {
print(error.localizedDescription)
}
}
func delete(_ object: NSManagedObject) {
context.delete(object)
save()
}
}
To fetch data
PersistenceManager.shared.fetch(User.self)
To delete data
PersistenceManager.shared.delete(user)
To create user
let newUser = Users(context: PersistenceManager.shared.context)
newUser.name = "Zero Cool"
newUser.password = "qwerty"
PersistenceManager.shared.save()

Unable to save values to Core Data

I am trying to store data to Core-Data in AppDelegate itself and then fetch that data from ViewController. For some reason, it is not getting saved in Core-Data. I have tried searching for this issue on the internet but did not get any specific solution. I have pasted the code below -
AppDelegate.swift
func saveContext () {
let context = persistentContainer.viewContext
print(context)
let entity = NSEntityDescription.entity(forEntityName: "Msg", in: context)
print(entity)
let new = NSManagedObject(entity: entity!, insertInto: context)
print(new)
print(getData.alert_message)
new.setValue(getData.alert_message, forKey: "title")
do {
try context.save()
print("save")
print(new.value(forKey: "title"))
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
Console Output
You can fetch record from the entity by NSFetchRequest
Try with below method, You just need to pass name of the entity as argument of the function.
func getAllRecordFromTableWhere(_ tableName: String) -> [NSManagedObject]? {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: tableName)
do {
return try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
return nil
}
and call function like
if let arrayOfData = DBHelper.getAllRecordFromTableWhere("Msg") {
print(arrayOfData)
}
Hear I have Set My Own Solution, Hope This help You
let contex = ((UIApplication.shared.delegate) as! AppDelegate).persistentContainer.viewContext
func SaveData()
{
let entity = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: contex);
entity.setValue(txtname.text, forKey: "empname") // txtname.text is My TexField name
entity.setValue(txtadd.text, forKey: "empadd");
do
{
try contex.save();
}
catch
{
}
}
I think you are not giving values to all attributes of the entity so check that first.
try something like
new.setValue(msgdata, forKey: "msgdata")
//msgdata is what you want to save on Coredata entity
//try something like this
In my case i was trying to update data saved in core data. but every time it was returning the old data ( not the updated version )
Saved successfully message was being printed in console same as yours
Here are some simple steps that finally solved my problem
fetch data with a NSFetchRequest
save data into a temp variable
delete old data from core data
modify data from temp variable
and finally save context
Here is my update data function
func updateUserData(_ userDetails: UserModel) {
let request = UserDetails.fetchRequest() as NSFetchRequest<UserDetails>
do {
let result = try persistentContainer.viewContext.fetch(request)
if let data = result.first?.userData as Data? {
var loginModel = LoginModel(data: data)
loginModel?.user = userDetails
self.clearUserData() //deleting old data
self.userLoginData = loginModel?.jsonData
}
}catch let error as NSError {
debugPrint(error.localizedDescription)
}
}

GCD and Core Data

I'm trying to solve a problem where fetching data from CoreData executes earlier than retrieving data from Parse and saving it to CoreData. How do I implement a queue to perform fetching data From Parse earlier? Now when I launch the app the database is updated, but it is not shown in the tableView.
Retrieving data from CoreData:
func fetchFromCoreData() {
do {
let results = try context.executeFetchRequest(fetchRequest)
medicines = results as! [Medicine]
print("FetchFromCoreData")
tableViewMedicines.reloadData()
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
Fetching data From Parse:
func fetchFromParse() {
let entity = NSEntityDescription.entityForName("Medicine", inManagedObjectContext: context)
let query = PFQuery(className: PFUser.currentUser()!.username!)
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
for object in objects! {
if let name = object["medicineName"] as? String,
amount = object["amountQuantity"] as? String,
time = object["time"] as? String
{
let predicate = NSPredicate(format: "name = %#", name)
self.fetchRequest.predicate = predicate
do{
let fetchedEntities = try self.context.executeFetchRequest(self.fetchRequest) as! [Medicine]
//save to Core Data
if fetchedEntities.count <= 0 {
let medicine = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: self.context)
medicine.setValue(name, forKey: "name")
medicine.setValue(amount, forKey: "amount")
medicine.setValue(time, forKey: "time")
}
} catch let error as NSError{
print(error)
}
}
}
}
do {
try self.context.save()
print("Context.save")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
self.fetchFromCoreData()
}
}
I'm calling them in viewWillAppear:
override func viewWillAppear(animated: Bool) {
if Reachability.isConnectedToNetwork() {
//fetching data from Parse
fetchFromParse()
fetchFromCoreData()
tableViewMedicines.reloadData()
} else {
//fetching data from Core data
fetchFromCoreData()
logOutButton.enabled = false
}
}
This is happening because findObjectsInBackgroundWithBlock makes a network call and only executes its block later on, asynchronously, once the network call completes. Your code executes in this order:
Call fetchFromParse()
In fetchFromParse(), call Parse. Parse goes off and talks to the network in the background...
Call fetchFromCoreData()
At some point later on, Parse gets a network response, and your block executes.
You should expect that async network calls will always take a long time, and that callbacks from network activity will always be delayed to some unknown time in the future. Meaning, you can't somehow move step 4 above earlier in the list.
GCD is not the answer here. You have to wait until the network activity finishes before you can process results from Parse. In the meantime, your app needs to be responsive, so you can't just sit around waiting for that to happen.
What you need to do is re-do the Core Data fetch after the Parse call completes. Reloading the table view isn't enough if you're still displaying the results of the earlier fetch.

How to update existing object in core data ? [Swift]

I have preloaded data from a .csv file into coredata. I am fetching the data in the following way
var places:[Places] = []
in viewDidLoad :
if let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext {
let fetchRequest = NSFetchRequest(entityName: "Places")
do{
places = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Places]
}
catch let error as NSError{
print("Failed to retrieve record: \(error.localizedDescription)")
}
}
In the data there is an attribute isFavorite of type String whose initial value is false. I am changing the value of isFavorite on button click. I want to save the changes made by the user. How can i make this change persistent ?
Here is my button action
#IBAction func addToFavourites(sender: AnyObject) {
cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: sender.tag, inSection: 0)) as! CustomTableViewCell
if cell.isFavouriteLabel.text! == "false" {
cell.isFavouriteLabel.text = "true"
}else if cell.isFavouriteLabel.text == "true"{
cell.isFavouriteLabel.text = "false"
}
}
Basically i want to set the value of places.isFavourite = cell.isFavoriteLabel.text and save to core data
EDIT : if i try this inside my button action method
if let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext {
let place : Places = Places()
place.isFavourite = cell.isFavouriteLabel.text
do{
try managedObjectContext.save()
} catch let error as NSError{
print(error)
}
}
i am getting an error : Failed to call designated initializer on NSManagedObject class
if i simply add this code in the button action method
places.isFavourite = cell.isFavouriteLabel.text
i get this error : [Places] does not have a member named isFavourite
Your current code is:
if let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext {
let place : Places = Places()
place.isFavourite = cell.isFavouriteLabel.text
do{
try managedObjectContext.save()
} catch let error as NSError{
print(error)
}
}
That would create a new place (if it worked), but you need to update an existing one.
You have the places returned from managedObjectContext.executeFetchRequest.
So you need to get something like places[index_of_the_cell_in_question].isFavourite = cell.isFavouriteLabel.text
and then managedObjectContext.save().
Use the save function of the NSManagedObjectContext:
places.isFavourite = cell.isFavoriteLabel.text
var error: NSError?
if managedObjectContext.save(&error) != true {
// Error
}
This is simple as this:
Find the entry you want to modify in places then save the core data context.
func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
println("Unresolved error \(error), \(error!.userInfo)")
abort()
}
}
}
I suggest you used a manager to insert, fetch and delete entry in your core data.
import Foundation
import CoreData
class CoreDataHelper: NSObject {
class var shareInstance:CoreDataHelper {
struct Static {
static let instance:CoreDataHelper = CoreDataHelper()
}
return Static.instance
}
//MARK: - Insert -
func insertEntityForName(entityName:String) -> AnyObject {
return NSEntityDescription.insertNewObjectForEntityForName(entityName, inManagedObjectContext: self.managedObjectContext!)
}
//MARK: - Fetch -
func fetchEntitiesForName(entityName:String) -> NSArray {
...
}
//MARK: - Delete -
func deleteObject(object:NSManagedObject) {
self.managedObjectContext!.deleteObject(object)
}
// MARK: - Core Data Saving support -
func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
println("Unresolved error \(error), \(error!.userInfo)")
abort()
}
}
}
Hop this can help you

Resources