understanding adding predicate to NSFetchrequest in swift - ios

I want to store some data downloaded from a remote file in CoreData. his data may already have been downloaded before and then the changes need to be added to core data.
I use the code displayed here. In three places I use a Fetchrequest to check if the data is already in the database. I use a predicate with the request. In the code I added comments to distinguish between them. If the item is already in coreData I update the data, else I instantiates a new item. The code to add the item to core data is in a convenience init in the class. This works fine. The problem I am getting is this:
The first time in this code id equals 0 so the schoolFetch is performed. Adding the predicate nr 1 gives no error and the school class is instantiated with its id set to 0.
Second time id = 1. This means a locationItem, with location being a part of the school, is checked for and instantiated. To add the location to the school a fetchrequest is performed to get the schoolInstance. Predicate 2 is used. This gives no error although the previous added schoolInstance is not retrieved, but it is if the predicate is not used (commented out).
After the schoolInstance is retrieved I use a fetchrequest to check if the location is already in coredata. If not it is instantiated, else its data is updated. The predicate nr 3 gives a run-time error stating nothing but EXC_BAD_ACCESS (code=1, address = 0x1). No error description is given in the debugger window.
So I have two questions:
a) why does predicate nr 2 not return the previous inserted item?
b) why does predicate nr 3 give an error?
I structured this code this way as it is possible to first receive the information on the location and after that the information on the school.
if id == 0 {
//get school
let schoolFetch = NSFetchRequest<StreekgidsSchool>(entityName: "School")
schoolFetch.predicate = NSPredicate(format: "locationId == %#", id) // predicate 1
do {
let fetchedSchool = try streekgidsModel?.context.fetch(schoolFetch)
if let school = fetchedSchool?.first{
school.name = name
school.locationId = id
school.colorValue = color
schoolInstance = school
}
else {
//create new entry in coredata for school
schoolInstance = StreekgidsSchool(name: name, context: (streekgidsModel?.context)!)
schoolInstance?.locationId = id
schoolInstance?.colorValue = color
}
} catch {
fatalError("Failed to fetch Schooldata: \(error)")
}
}
else {
//check if school already is defined
let schoolFetch = NSFetchRequest<StreekgidsSchool>(entityName: "School")
schoolFetch.predicate = NSPredicate(format: "locationId == %#", 0) //predicate 2
do {
let fetchedSchool = try streekgidsModel?.context.fetch(schoolFetch)
if let school = fetchedSchool?.first{
schoolInstance = school
}
else {
//create new entry in coredata for school
schoolInstance = StreekgidsSchool(id: 0, context: (streekgidsModel?.context)!)
}
} catch {
fatalError("Failed to fetch Schooldata: \(error)")
}
//get location
let locationFetch = NSFetchRequest<StreekgidsLocation>(entityName: "Location")
locationFetch.predicate = NSPredicate(format: "locationId == %#", id) //predicate 3
do {
let fetchedLocation = try streekgidsModel?.context.fetch(locationFetch)
if let location = fetchedLocation?.first{
location.name = name
location.locationId = id
location.colorValue = color
location.school = schoolInstance
}
else {
//create new entry in coredata for location
let locationInstance = StreekgidsLocation(name: name, context: (streekgidsModel?.context)!)
locationInstance.locationId = id
locationInstance.colorValue = color
}
} catch {
fatalError("Failed to fetch Schooldata: \(error)")
}
}

Exactly. The %# is expecting a Foundation object as the argument but an Int is not an object. So your id (set to zero) is being interpreted as nil. That is also why your predicate 2 is returning nothing and why you retrieve the result when you comment it out. Only your third predicate throws a run-time error but in fact all three of your predicates are working incorrectly, the first two are simply not falling over in an immediately visible way. Have you tried substituting %# for %d.
And as you point out %u for unsigned integers.

Related

Comparing strings with NSPredicate before saving to Core Data

I am trying to perform a string comparison before saving to Core Data.
The string that gets saved to Core Data will contain a list of physical exercises. The string of exercises must only get saved once regardless of order.
Example:
let str1 = "Burpees Rowing Running"
// This is in Core Data
let str2 = "Running Rowing Burpees"
// This is an attempt to save to Core Data. It should *fail* because there is already an exercise set with these exercises - just not in the same order.
My progress:
func checkEntityThenSave(exerciseGroup:String){
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "SavedExerciseSets")
let predicate = NSPredicate(format: "upperBody.sortedComponents == %#", exerciseGroup.components(separatedBy: " ").sorted())
request.predicate = predicate
request.fetchLimit = 1
do{
let count = try context.count(for: request)
print("Count - \(count)") // Always evaluates to 0
if(count > 0){
// Save to Core Data
}
else{
// Show Alert
}
}
catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
In my code, I am trying to compare the fetched result (string) in Core Data, with the new string I am attempting to save.
My problem is I keep getting 0 - which is causing the save attempt to fail every time.
How can I compare a string that I am trying to save, with a string that occurs in Core Data?
Assuming your sortedComponents is of type String, perhaps you can try this:
let sortedExerciseGroup = exerciseGroup.components(separatedBy: " ").sorted().joined(separator: " ")
let predicate = NSPredicate(format: "upperBody.sortedComponents == %#", sortedExerciseGroup)
But, if your sortedComponents is of type [String] the code in the question should already work, just make sure that you save to core data the exerciseGroup that is already sorted.

check of the data is saved on the CoreData or not

i have a string called "word" and i stored this string into a an entity called Lose inside an attribute word,
i want when the user save a string "word" again it will overwrite the current value not sitting a duplicate one, how to do that ?
what i have done so far:
//save the data to core data
let library = Lose(context: savecatch.context)
library.word = "word"
savecatch.saveContext()
before the saving i need to check if "word" is already exist or not
You have to fetch the record with the given attribute, then modify it and save it
let query = "word"
let request = NSFetchRequest<Lose> = Lose.fetchRequest()
request.predicate = NSPredicate(format: "word == %#", query)
do {
let result = try context.fetch(request)
if let found = result.first {
found.word = "word"
context.save()
}
} catch {
print(error)
}

Loop in different type of array to find matched one

I'm trying to add a favorite button to my application using CoreData.
I have tableView and added favorite button inside of it to save the label when I press that specific row. I saved it successfully. But I want to populate the CoreData just once for that specific row.
var checkFav = [Fav]()
I created an array with Type Fav which is name of my class for CoreData to populate items I have to check they appear just once.
let result = try context.fetch(Fav.fetchRequest())
checkFav = result as! [Fav]
if checkFav.isEmpty{
let fav = Fav(context: context)
fav.name = name
appDelegate.saveContext()
}
Above you see i populated the array.
do{
let result = try context.fetch(Fav.fetchRequest())
checkFav = result as! [Fav]
if checkFav.isEmpty{
let fav = Fav(context: context)
fav.name = name
appDelegate.saveContext()
}
else{
let checkName = array[rowSelected]
for value in checkFav{
if value.name == checkName{
print("You already have this name ")
}
else {
let fav = Fav(context: context)
fav.name = name
appDelegate.saveContext()
}
}
}
} catch{
print("Error")
}
Let's says I have two name "john","martha" in CoreData if I press to button of "martha" it shouldn't add again. But because of my loop when it sees "john" in the array it thinks this name isn't matching so it's saving "martha" (same name) to CoreData.
How can I check my checkFav array which contains the upcoming name if it contains don't save it again. But if not, add to CoreData. I'm really confused.
Your way to check for an empty entity cannot work if you want to save multiple records.
Add a predicate to the fetch request and a result limit of 1 to check for the specific name.
This method takes the name as parameter and adds a record if the name does not exist. It returns true if an entry is added otherwise false. Further it passes a potential error.
func addFavorite(for name : String) throws -> Bool {
let request : NSFetchRequest<Fav> = Fav.fetchRequest()
request.predicate = NSPredicate(format: "name = %#", name)
request.resultsLimit = 1
if let _ = try context.fetch(request).first {
return false // record exists
} else {
let fav = Fav(context: context)
fav.name = name
appDelegate.saveContext()
return true // record added
}
}

FetchRequst issue with data fault

When I was inserting data to one entity of CoreData, All the rows are inserted successfully(Saved).
But when I try to fetch the data using FetchRequest, Only one row of data is coming even if number of rows inserted are 3 or 4 or anything(more than 1).
Remaining rows are not getting fetched. And when I print fetch results,
It says - Error
0:<EquipmentDetails: 0x6000000bad60>
(entity: EquipmentDetails; id: 0xd000000000040000
coredata:/EquipmentDetails/p1> **data:fault>)**
I didn't get what was going in backend of core data?
code for Insertion
func insertEqipToLocalDb()
{
let mobileNo : String = UserDefaults.standard.string(forKey: "phoneNumber")!
let equipDetailsItem = NSEntityDescription.insertNewObject(forEntityName: "EquipmentDetails", into:managedObjContext) as! EquipmentDetails
for (index,item) in array_IDEquip.enumerated()
{
equipDetailsItem.mobileNumber = mobileNo
equipDetailsItem.type = array_typeEquip[index]
equipDetailsItem.name = array_nameEquip[index]
equipDetailsItem.startDate = array_sDateEquip[index]
equipDetailsItem.endDate = array_eDateEquip[index]
equipDetailsItem.equpID = Int16(item)
equipDetailsItem.serviceDatesStr = array_serviceDateEquip[index]
}
do
{
try managedObjContext.save()
UserDefaults.standard.set("AlreadyInstalled", forKey: "statusInstallation")
}
catch
{
Exception.insertExceptionDetails(errorMsg: error as NSError, context: managedObjContext)
}
}
//code for fetching
let request = NSFetchRequest<NSFetchRequestResult>()
let entity = NSEntityDescription.entity(forEntityName:"EquipmentDetails", in: managedObjContext)
request.entity = entity
do
{
let fetchResults = try managedObjContext.fetch(request)
for r in fetchResults
{
typeEquipArray.append((r as AnyObject).value(forKey: "type") as! String)
}
}
catch let error as NSError
{
Exception.insertExceptionDetails(errorMsg: error, context: managedObjContext)
}
On this line:
let equipDetailsItem = NSEntityDescription.insertNewObject(forEntityName: "EquipmentDetails", into:managedObjContext) as! EquipmentDetails
You create one instance. In the loop that follows, you set values for the type, name, etc properties over and over again on that same instance. Then you save changes, which include just that one object. If you want a difference instance of EquipmentDetails for each pass through the loop, you need to create the instance inside the loop.
The "fault" message is not an error unless you tried to access the property values and found that they were not present. It's part of how Core Data works. See the answer that Harshal Valanda linked in the comments for more detail.

Faster way to check if entry exists in Core Data

In my app when data is synced i can get 20k entries (from given timestamp) from the server that should be synced to the local device. For every entry i try to fetch it (if it exist already) and if doesn't i create new. The problem is that the whole operation is too slow - for 20k on iphone 5 is 10+ mins. Another solution that i though is to delete all entries from the given timestamp and create new entries for all returned entries and there will be no need to perform fetch for every single entry ? If someone have any advice will be nice. Here is sample code for the current state:
var logEntryToUpdate:LogEntry!
if let savedEntry = CoreDataRequestHelper.getLogEntryByID(inputID: inputID, fetchAsync: true) {
logEntryToUpdate = savedEntry
} else {
logEntryToUpdate = LogEntry(entity: logEntryEntity!, insertInto: CoreDataStack.sharedInstance.saveManagedObjectContext)
}
logEntryToUpdate.populateWithSyncedData(data: row, startCol: 1)
Here is the actual request method:
class func getLogEntryByID(inputID:Int64, fetchAsync:Bool) ->LogEntry? {
let logEntryRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "LogEntry")
logEntryRequest.predicate = NSPredicate(format: "inputId == %#", NSNumber(value: inputID as Int64))
logEntryRequest.fetchLimit = 1
do {
let mocToFetch = fetchAsync ? CoreDataStack.sharedInstance.saveManagedObjectContext : CoreDataStack.sharedInstance.managedObjectContext
if let fetchResults = try mocToFetch.fetch(logEntryRequest) as? [LogEntry] {
if ( fetchResults.count > 0 ) {
return fetchResults[0]
}
return nil
}
} catch let error as NSError {
NSLog("Error fetching Log Entries by inputID from core data !!! \(error.localizedDescription)")
}
return nil
}
Another thing that i tried is to check the count for specific request but again is too slow.
class func doesLogEntryExist(inputID:Int64, fetchAsync:Bool) ->Bool {
let logEntryRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "LogEntry")
logEntryRequest.predicate = NSPredicate(format: "inputId == %#", NSNumber(value: inputID as Int64))
//logEntryRequest.resultType = .countResultType
logEntryRequest.fetchLimit = 1
do {
let mocToFetch = fetchAsync ? CoreDataStack.sharedInstance.saveManagedObjectContext : CoreDataStack.sharedInstance.managedObjectContext
let count = try mocToFetch.count(for: logEntryRequest)
if ( count > 0 ) {
return true
}
return false
} catch let error as NSError {
NSLog("Error fetching Log Entries by inputID from core data !!! \(error.localizedDescription)")
}
return false
}
Whether fetching the instance or getting the count, you're still doing one fetch request per incoming record. That's going to be slow, and your code will be spending almost all of its time performing fetches.
One improvement is to batch up the records to reduce the number of fetches. Get multiple record IDs into an array, and then fetch all of them at once with a predicate like
NSPredicate(format: "inputId IN %#", inputIdArray)
Then go through the results of the fetch to see which IDs were found. Accumulate 50 or 100 IDs in the array, and you'll reduce the number of fetches by 50x or 100x.
Deleting all the entries for the timestamp and then re-inserting them might be good, but it's hard to predict. You'll have to insert all 20,000. Is that faster or slower than reducing the number of fetches? It's impossible to say for sure.
Based on Paulw11's comment, I came up with the following method to evaluate Structs being imported into Core Data.
In my example, I have a class where I store search terms. Within the search class, create a predicate which describes the values of the stuff within my array of structs.
func importToCoreData(dataToEvaluateArray: [YourDataStruct]) {
// This is what Paul described in his comment
let newDataToEvaluate = Set(dataToEvaluateArray.map{$0.id})
let recordsInCoreData = getIdSetForCurrentPredicate()
let newRecords = newDataToEvaluate.subtracting(recordsInCoreData)
// create an empty array
var itemsToImportArray: [YourDataStruct] = []
// and dump records with ids contained in newRecords into it
dataToEvaluateArray.forEach{ record in
if newRecords.contains(record.id) {
itemsToImportArray.append(record)
}
}
// THEN, import if you need to
itemsToImportArray.forEach { struct in
// set up your entity, properties, etc.
}
// Once it's imported, save
// You can save each time you import a record, but it'll go faster if you do it once.
do {
try self.managedObjectContext.save()
} catch let error {
self.delegate?.errorAlert(error.localizedDescription, sender: self)
}
self.delegate?.updateFetchedResultsController()
}
To instantiate recordsInCoreData, I created this method, which returns a Set of unique identifiers that exist in the managedObjectContext:
func getIdSetForCurrentPredicate() -> Set<String> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "YourEntity")
// searchQuery is a class I created with a computed property for a creating a predicate. You'll need to write your own predicate
fetchRequest.predicate = searchQuery.predicate
var existingIds: [YourEntity] = []
do {
existingIds = try managedObjectContext.fetch(fetchRequest) as! [YourEntity]
} catch let error {
delegate?.errorAlert(error.localizedDescription, sender: self)
}
return Set<String>(existingIds.map{$0.id})
}

Resources