Architecture of an app using Core Data and Parse - ios

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.

Related

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

Core Data fetch returns zero records though table is not empty

I'm a Swift newbie, building a Core Data app. I've built and run a sample app from Ray Wenderlich's site which works as expected.
I've lifted and modified the following excerpt from Ray's app:
var people: [NSManagedObject] = []
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//1
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
// 2
let fetchrequest = NSFetchRequest<NSManagedObject>(entityName: "Person")
// 3
do {
people = try managedContext.fetch(fetchrequest)
print("\(people.count) fetched from Person")
}
catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
This works as expected. However, the following is what I've implemented in my app:
var players: [NSManagedObject] = []
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Golfer")
do {
players = try managedContext.fetch(fetchRequest)
print("\(players.count) fetched from Golfer")
// Load any saved players, otherwise load sample data.
if players.count == 0 {
// Load the sample data.
// There are no players to load
os_log("Loading sample players", log: OSLog.default, type: .debug)
loadSamplePlayers()
}
}
catch let error as NSError {
print("Could not fetch Golfers. \(error), \(error.userInfo)")
}
}
The sample code always returns the full results of the Person table and the person array count is always equal to the number of rows in the table.
However, for my version, it always reports that the players array has a count of zero. Consequently, the loadSamplePlayers() method is called each time (which adds three sample records into the Golfer table).
Using an SQL Lite database inspector, I can see that the Golfer table continues to grow after each invocation of the app.
I'm stumped on why these two, near-identical, code samples should behave differently.
Any insights would be much appreciated.
Thanks!
EDIT:
Here's the loadSamplePlayers() method and printDatabaseStats() which is also referenced:
private func loadSamplePlayers() {
let sampleFirstNames: [String] = ["Casper", "Jocelyn", "Arthur"]
let sampleLastNames: [String] = ["Vitturio", "Brown", "Mullard"]
let sampleEmails: [String] = ["a#b.com", "fred#x.co.uk", "last#ohm.com"]
let sampleHandicaps: [Double] = [16.2, 6.4, 27.2]
let entity = NSEntityDescription.entity(forEntityName: "Golfer", in: managedObjectContext!)!
for i in 1...3 {
let player = NSManagedObject(entity: entity, insertInto: managedObjectContext)
player.setValue(sampleFirstNames[i-1], forKeyPath: "firstName")
player.setValue(sampleLastNames[i-1], forKey: "lastName")
player.setValue(sampleEmails[i-1], forKey: "emailAddress")
player.setValue(sampleHandicaps[i-1], forKey: "exactHandicap")
// player.setValue(sampleMugshots[i-1], forKey: "mugShot")
print("Prepare to save")
do {
try managedObjectContext?.save()
players.append(player)
}
catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
print("There are now \(printDatabaseStats()) in the Golfers table")
}
}
private func printDatabaseStats() -> Int {
var count = 0
managedObjectContext?.performAndWait {
if let golferCount = try? self.managedObjectContext!.count(for: NSFetchRequest(entityName: "Golfer")) {
print("\(golferCount) Payers")
count = golferCount
}
}
return count
}
This appears to work successfully as the print(...) methods produce the expected console messages and the Golfer table is populated and contains the rows as defined.
Not included in the original question was a class definition, thus:
var managedObjectContext: NSManagedObjectContext? = (UIApplication.shared.delegate as? AppDelegate)?.managedObjectContext
This is defined differently to:
let managedContext = appDelegate.persistentContainer.viewContext
yet I was using them as if they were the same. I normalised the two so they are identical and the problem has gone.

Swift 3 / iOS 10, Proper Core Data Usage

Before upgrading my apps to Swift 3 and iOS 10, I had no problems with using CoreData as a data store for simple objects. Lightweight Migrations were simple, saving and fetching was simple, ect. But ever since the recent upgrade, I have had nothing but trouble with CoreData.
My question is in two parts. First, does anyone know of any good resources to help me learn how CoreData works behind the scenes so I can get better at debugging it? Apple's docs are extremely limiting, and all the articles I read act like the new CoreData is so simple. I have decent experience with relational databases, so CoreData is adding an uncomfortable layer of abstraction for me.
Second, what is wrong with the following code? Lightweight migrations aren't working as they did before iOS 10, using this code. Objects are saving to CoreData (I can interact with them in app after saving), but then disappear after the app is restarted.
lazy var persistentContainer: NSPersistentContainer = {
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
let container = NSPersistentContainer(name: "MY_APP")
container.persistentStoreDescriptions = [description]
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
I'm using a separate file to abstract the storing of my objects:
class Repository
{
func getContext () -> NSManagedObjectContext
{
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
func delete<T>(_ a: some T)
{
getContext().delete(a as! NSManagedObject)
}
// ----------- Password Repo ----------
func savePassword(name: String, hint: String, un: String) {
let context = getContext()
//retrieve the entity that we just created
let entity = NSEntityDescription.entity(forEntityName: "Password", in: context)
let transc = NSManagedObject(entity: entity!, insertInto: context)
//set the entity values
transc.setValue(name, forKey: "name")
transc.setValue(hint, forKey: "thing")
transc.setValue(un, forKey: "newThing")
//save the object
do {
try context.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
}
func updatePassword(pas: Password) -> Password
{
let context = getContext()
// sec.is_new = false
// TODO, add updates
// Try updating the model in the DB
do {
try context.save()
} catch {
print(error)
}
return pas
}
func fetchPasswords() -> [Password]
{
let context = getContext()
//create a fetch request, telling it about the entity
let fetchRequest: NSFetchRequest<Password> = Password.fetchRequest() as! NSFetchRequest<Password>
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
//go get the results
let searchResults = try getContext().fetch(fetchRequest)
return searchResults
} catch {
print("Error with request: \(error)")
}
return []
}
// ----------- End Password Repo ----------
// ----------- Hints Repo ----------
func saveHint (name: String, hint: String) {
let context = getContext()
//retrieve the entity that we just created
let entity = NSEntityDescription.entity(forEntityName: "Hint", in: context)
let transc = NSManagedObject(entity: entity!, insertInto: context)
//set the entity values
transc.setValue(value1, forKey: "some_string")
transc.setValue(value2, forKey: "some_thing")
//save the object
do {
try context.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
}
func fetchHints() -> [Hint]
{
let context = getContext()
//create a fetch request, telling it about the entity
let fetchRequest: NSFetchRequest<Hint> = Hint.fetchRequest() as! NSFetchRequest<Hint>
let sortDescriptor = NSSortDescriptor(key: "my_key", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
//go get the results
let searchResults = try getContext().fetch(fetchRequest)
return searchResults
} catch {
print("Error with request: \(error)")
}
return []
}
}
Then I call this Repository class like so:
Repository().savePassword(name: nameText.text!, hint: hintSoFarLabel.text!, un: "Hey")
The Repository class is working, until I restart the app...
I'm also trying to migrate to a new version of my Core Data Model which simply adds a non-optional String attribute (with default value), which would be a simple lightweight migration in iOS 9/Swift 2. What am I missing about swift 3 lightweight migrations?
I have no doubt that the problem is me not understanding CoreData in iOS 10 well enough. I've been a software engineer for a while, but I've only been working with iOS and Swift for a few months, so please be gentle. And thank's in advance!
Well, I feel like an idiot. I added a non-optional string and didn't give it a default value. I always gave default values to non-string types, but the way Xcode is set up made it appear that it would just give strings a default value of "" if no other was given.
Adding a default value to the new attribute allowed the migration to work in Swift3/iOS10. Noob mistake, but maybe it will help someone here in the future.

(iOS Swift) Fetching record from Core Data return fatal error or nil

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

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.

Resources