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.
Related
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)
}
}
I'm having issues trying to append a core data entity called products - I used the same code that I used to add companies, which works fine. The only difference in the app is that there is just one "static" tableview of companies I guess you could say, whereas the tableview for products is dynamically set depending on which company cell is tapped. But I'm not sure how that would cause the problem.
I've been debugging/checking values all over the app since yesterday and it seems like no matter what I try to alter, products.count in numberOfRowsInSection remains 0.
I call a function called handleSave() when the user hits a done button after entering new values in three text fields, just like with my other object companies:
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
let cc = UINavigationController()
let companyController = CompanyController()
viewController = companyController
cc.viewControllers = [companyController]
present(cc, animated: true, completion: nil)
}
This then calls the save function to append products:
func save(name: String, url: URL, image: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Product",
in: managedContext)!
let product = NSManagedObject(entity: entity,
insertInto: managedContext)
product.setValue(name, forKey: "name")
product.setValue(url, forKey: "url")
product.setValue(image, forKey: "image")
do {
try managedContext.save()
products.append(product)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
tableView.reloadData()
}
And the data is fetched in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let companyToDisplay = self.navigationItem.title!
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Product")
fetchRequest.predicate = NSPredicate(format:"company.name == %#",companyToDisplay)
do {
products = try managedContext.fetch(fetchRequest)
print(products)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
When this exact same process is used for adding companies, the new company then appears in the tableview. However now with products, that is not the case; the tableview remains empty. I'm not sure what the difference is and I'm not experienced enough with core data that I feel like I can figure out where the problem lies.
Huge thanks in advance to anyone that can figure this out!
After invoking handleSave() & before performing table.reloadData(), you need to perform db fetch operations done in viewWillAppear().
Segregate your fetchRequest logic out of viewWillAppear() into a function called (say) fetchUpdatedData().
Now, after try managedContext.save(), do a fetchUpdatedData(), update your product data arrays & then update the UI by table.reloadData()
Use a NSFetchedResultsController to keep your view in sync with your model. NSFetchedResultsController monitors core-data and sends a delegate message when there are updates - then you can update your tableview. Otherwise you have to manually do a refresh whenever core-data changes.
I'm starting to code with Swift for iPhone apps, i'm facing this rather confusing obstacle that for some of you might be trivial. I keep getting error message that the record is either fatal error: unexpectedly found nil while unwrapping an Optional value or nil, but when i check the .sqlite the record is there
Let me walk you through
The name of my .xcdatamodeld is ReviewerApp.xcdatamodeld, the same as my app name ReviewApp
ReviewerApp.xcdatamodeld
My class name is Users and my Entity name is User
Class: Users, Entity: User
My Attributes (username, email, password) are all type: String, with properties: optional
Users.swift
import Foundation
import CoreData
#objc(Users)
class Users: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
#NSManaged var username: String?
#NSManaged var email: String?
#NSManaged var password: String?
}
The saveUser() function in signUpController.swift
func saveUser() {
// create an instance of our managedObjectContext
let moc = DataController().managedObjectContext
// we set up our entity by selecting the entity and context that we're targeting
let entity = NSEntityDescription.insertNewObjectForEntityForName("User", inManagedObjectContext: moc) as! Users
// add our data
entity.setValue(usernameTxtFld.text, forKey: "username")
entity.setValue(emailTxtFld.text, forKey: "email")
entity.setValue(passwordTxtFld.text, forKey: "password")
// we save our entity
do {
try moc.save()
//user.append(entity)
print("saved")
} catch {
fatalError("Failure to save context: \(error)")
}
}
The fetch() function in SignInController.swift
let moc = DataController().managedObjectContext
let userFetch = NSFetchRequest(entityName: "User")
do {
let fetchedUser = try moc.executeFetchRequest(userFetch) as! [Users]
print(fetchedUser.first!.username!)
} catch {
fatalError("Failed to fetch person: \(error)")
}
Everytime i save the user registration process, the record is saved in core data. But everytime i am trying to fetch it:
by: print(fetchedUser.first!.username!) , the message in the console is fatal error: unexpectedly found nil while unwrapping an Optional value
by: print(fetchedUser.first?.username) , the message in the console is nil
Thank you very much for the help, in advance
First I would highly suggest you not to using '!' anywhere in your code as it will crash your app (only for static resources). You can use 'if let XXX = YYY as? [...]' to safely cast whatever you want...
Then you are probably not saving anything in your Database. How is your model? Yours Users class must be adequate to the model you built otherwise it won't work (which is a pain), also in your model you have to give your entity its class (in the 'class' field just below 'parent entity').
Also you don't have to use 'entity.setValue(usernameTxtFld.text, forKey: "username")' as you casted your entity beforehand. You could just 'entity.username =usernameTxtFld.text'.
If I didn't help, try to also add the xcdatamodel please.
I think you missed out on
let entityDescription = NSEntityDescription.entityForName("User", inManagedObjectContext: moc)
fetchRequest.entity = entityDescription
do {
let fetchedUser = try moc.executeFetchRequest(fetchRequest)
print(fetchedUser.first!.username!)
} catch {
let fetchError = error as NSError
print(fetchError)
}
The method returns an array of results if the fetch request is successful. Note that Core Data always returns an array if the fetch request is successful, even if we expect one result or if Core Data didn't find any matching records.
For more information and clarification visit - http://code.tutsplus.com/tutorials/core-data-and-swift-managed-objects-and-fetch-requests--cms-25068
The case is closed. There is some problem with the DataController.swift. It was called to save the data, but when it is called to fetch the data it somehow pointing to different place. So, what i did was
Not use the DataController.swift anymore
Equipped my AppDelegate.swift with // MARK: - Core Data Saving support for saving context, because i didn't set my app with including core data in the beginning
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
// 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.
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
I changed my saving function to
func saveUser() {
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let entity = NSEntityDescription.insertNewObjectForEntityForName("Users", inManagedObjectContext: managedContext) as! Users
//3
entity.setValue(usernameTxtFld.text, forKey: "username")
entity.setValue(emailTxtFld.text, forKey: "email")
entity.setValue(passwordTxtFld.text, forKey: "password")
//4
do {
try managedContext.save()
//5
message = "Signing up is successful"
alertMessage(message)
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
And changed my fetch function to
func fetch() {
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let fetchRequest = NSFetchRequest(entityName: "Users")
let predicate = NSPredicate(format: "email == %# AND password == %#", emailTxtFld.text!, passwordTxtFld.text!)
fetchRequest.predicate = predicate
//3
do {
let results =
try managedContext.executeFetchRequest(fetchRequest)
user = results as! [Users]
let row = user.count
if row > 0 {
message = "You're signed in"
//self.performSegueWithIdentifier("signInIdentifier", sender: self)
}else{
message = "Email & password combination is incorrect!"
}
alertMessage(message)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
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
I struggle with with building a To do app that will download activities from Parse , save them with Core Data and then they will be showed on Apple Watch. I'd like to ask if this is a more or less proper approach to do it:
In viewWillLoad we check if there is an internet connection:
if TRUE we loop over activities in core data and compare them with those from Parse
if they compare we do nothing and prepare cells using info from Core Data
if they do not compare we add them to Core data and prepare cells
if FALSE we prepare cells with info from Core Data
I'm trying to implement my way, but have a problem. Data fetched from parse shown only at the second launch of app. The do not fetch and show at the same time.
My properties
var medicines : [Medicine] = [Medicine]()
var frc :NSFetchedResultsController!
var context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Medicine")
My methods to get the data:
// MARK: - Fetching
func fetchFromParse() {
let entity = NSEntityDescription.entityForName("Medicine", inManagedObjectContext: context)
let query = PFQuery(className: "Medicine")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
for object in objects! {
let medicine = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: self.context)
if let name = object["medicineName"] as? String,
amount = object["amountQuantity"] as? String {
//save to Core Data
medicine.setValue(name, forKey: "name")
medicine.setValue(amount, forKey: "amount")
do {
try self.context.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
}
}
}
}
func fetchFromCoreData() {
do {
let results = try context.executeFetchRequest(fetchRequest)
medicines = results as! [Medicine]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
I call them in viewWillAppear:
if Reachability.isConnectedToNetwork() {
//fetching data from Parse
fetchFromParse()
fetchFromCoreData()
} else {
//fetching data from Core data
fetchFromCoreData()
logOutButton.enabled = false
}
A better approach is to set up a background task to fetch the data with Parse and stick new entries in your core data store, and when new items are detected, to refresh the table on the main thread.