NSFetchedResultsController triggered by NSManagedObject's refreshObject. Why? - ios

I have a NSFetchedResultsController that fetches data from a CoreData store. When the frc is initialized I call performFetch and check the number of fetched objects in frc.fetchedObjects the result is 0 as it should be.
In another place in the code I call:
[obj.managedObjectContext refreshObject:obj mergeChanges:NO]
Although I'm not even sure it is needed it has the side effect of causing the frc to fetch some objects which were not fetched initially and shouldn't be fetched anyway considering the query used. These objects are exactly the same objects refreshObject:mergeChanges: was called with.
Why is this happening?
Edit:
It doesn't happen for this query:
query.predicate = [NSPredicate predicateWithFormat:#"(cartEntry != NULL) AND (urlWeb = NULL)", version, dateUrlExpire];
frc = [[NSFetchedResultsController alloc] initWithFetchRequest:query managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
[frc performFetch:&error];
But when I change the query to this version then it happens (urlWebVersion and urlWebDate are NULL for all records):
query.predicate = [NSPredicate predicateWithFormat:#"(cartEntry != NULL) AND ((urlWeb = NULL) OR (urlWebVersion != %#) OR (urlWebDate > %#))", version, dateUrlExpire];
* Edit #2 *
Here is a minimal code sample that shows the strange behavior (no error during execution):
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSError *error;
// Create an empty entity with the optional fields attr1 (string) and attr2 (date)
Entity *e = [NSEntityDescription insertNewObjectForEntityForName:#"Entity" inManagedObjectContext:[self managedObjectContext]];
// Save entity
[_managedObjectContext save:&error];
// Setup fetched results controller
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(attr1 != %#) AND (attr2 != %#)", #"", [NSDate new], nil];
NSFetchRequest *query = [[NSFetchRequest alloc] initWithEntityName:#"Entity"];
query.predicate = pred;
query.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"attr1" ascending:NO]];
frc = [[NSFetchedResultsController alloc] initWithFetchRequest:query managedObjectContext:_managedObjectContext sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
// Load data
[frc performFetch:&error];
// Output #1
NSLog(#"%ld", frc.fetchedObjects.count);
[_managedObjectContext refreshObject:e mergeChanges:NO];
return YES;
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#"%ld", controller.fetchedObjects.count);
}
Output:
2015-01-22 22:53:44.293 CoreDataBug[10740:1445186] 0
2015-01-22 22:53:44.317 CoreDataBug[10740:1445186] 1

Your problem is that NSPredicate has difficulty matching != to a nil value. For example, in your Edit 2 your line:
[NSPredicate predicateWithFormat:#"(attr1 != %#) AND (attr2 != %#)",
#"", [NSDate new], nil];
as you know, returns zero after the fetch request. (There's an extra nil there, but it doesn't affect anything). But changing it to:
NSPredicate *pred = [NSPredicate predicateWithFormat:
#"((attr1 != %#) || (attr1 == nil)) AND ((attr2 != %#) || (attr2 == nil))",
#"", [NSDate new]];
does return the entities, even though logically these are the same.
This is documented behavior, albeit hard to follow for something so counter-intuitive, see "Using Null Values" subsection in Predicate Programming Guide.
It's unclear why refreshing the object would cause the predicate to suddenly match; this may be a bug. However, given the above documentation, != should not be used for values that might be nil (unless checking != nil) without a paired || (x = nil). More than a little annoying and counter intuitive.

Related

NSPredicate to check property of first 5 objects in a Core Data Relationship

I am looking to create a predicate that can check the TypeID of the first 5 objects in a Core Data relationship.
Here is what I am trying, but it doesn't work:
int num = 5;
NSMutableArray *predicates = [[NSMutableArray alloc] init];
for (int i = 0; i < num; i++) {
[predicates addObject:[NSPredicate predicateWithFormat:#"SELF IN %# AND logs[%i].TypeID == 3", items, i]];
}
This gives the error:
error: SQLCore dispatchRequest: exception handling request:
< NSSQLFetchRequestContext: 0x281837aa0 > , Unsupported function
expression logs[0].TypeID with userInfo of (null) CoreData:
error: SQLCore dispatchRequest: exception handling request:
< NSSQLFetchRequestContext: 0x281837aa0 > , Unsupported function
expression logs[0].TypeID with userInfo of (null)
I realize I am probably doing this wrong, so is there a different way that I could be doing this using NSPredicate?
if you want only fetch 5. Set fetchLimit to 5. Maybe the following code can not run right away, but the principle is same. You can add a property or get function to assign the "logs.TypeID" like -(Int)myID {return logs[0].TypeID;} then "SELF IN %# AND myID == 3" would solve the problem.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Entity name" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF IN %# AND logs.TypeID == 3", items];
[fetchRequest setPredicate:predicate];
fetchRequest.fetchLimit = 5;
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
}
I’m not sure I understand the questions, but I don’t think it’s possible to do what you asked, at least not as worded. It doesn’t seem like it would be possible to inspect only five objects in the relationship and stop there, returning any number of matches or none at all, even if there are more in the database.
However, and I think this may be what you were getting at, it is possible to find five objects that are both in the items set and have at least one log with a typeID == 3. It can be done similar to what E.Coms proposed, except you need a subquery to handle the relationship. (I am assuming logs is a to-many relationship, either one-to-many or many-to-many).
Note that the following code has not been tested:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Entity name" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF IN %# AND SUBQUERY(logs, $log, $log.typeID == 3).#count != 0", items];
[fetchRequest setPredicate:predicate];
fetchRequest.fetchLimit = 5;
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
}

CoreData Sort by relation

My relation is shown on the picture below.
I'd like to sort Events first by those that are in active promotion and then by start date. Promotion is active if current date is between start and end date. Unfortunately, because of CoreData, I'm not able to use transient properties for sorting. In my controller I'm not using a fetch controller.
Is there any way to achieve that?
Update:
I've following sort descriptors:
// First is incorrect
[NSSortDescriptor(key: "promotion.start", ascending: false),
NSSortDescriptor(key: "start", ascending: true)]
Predicates (They're ok, though):
let promotionsPredicate =
NSPredicate(format: "(%# >= promotion.start && %# <= promotion.end) && " +
"(ANY promotion.cities.id == %#)", NSDate(), NSDate(), objectID)
let eventsPredicate =
NSPredicate(format: "start >= %# && venue.city.id == %#",
NSDate(), objectID)
let subpredicates = [eventsPredicate, promotionsPredicate]
let compoundPredicate NSCompoundPredicate(orPredicateWithSubpredicates: subpredicates)
And this is the Request (I'm using CoreStore, but the idea should be clear):
class func pagedEventsForPredicateSortedByInPromoAndStartDate(predicate: NSPredicate,
descriptors: [NSSortDescriptor],
fetchOffset: Int,
fetchLimit: Int) -> [Event] {
return CoreStore.fetchAll(From(Event),
Where(predicate),
OrderBy(descriptors),
Tweak { (fetchRequest) -> Void in
fetchRequest.fetchOffset = fetchOffset
fetchRequest.fetchLimit = fetchLimit
fetchRequest.returnsObjectsAsFaults = false
}) ?? []
}
As I understood you have to get all Event objects, but just in proper order. To do that with such complicated order, that includes relationship, as far as I know you have to fetch all Events and then sort them using NSArray's method
- (NSArray<ObjectType> *)sortedArrayUsingComparator:(NSComparator)cmptr
Here are the pieces of the code
1. Fetch from Core Data
// get the right context here
NSManagedObjectContext *yourContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
// extra line, predicate is nil by default, any other required predicate could be written here
request.predicate = nil;
__block NSArray *results = nil;
[yourContext performBlockAndWait:^{
NSError *error = nil;
results = [yourContext executeFetchRequest:request error:&error];
if (error) {
// handle error here
}
}];
Fetch is made manually with core methods and you may use Magical Record or any other framework that works with Core Data to make it in a row.
2. Sort the results
__weak typeof(self) weakSelf = self;
NSDate *now = [NSDate date];
NSArray *sortedResults = [results sortedArrayUsingComparator:^NSComparisonResult(Event *_Nonnull obj1, Event *_Nonnull obj2) {
BOOL isObj1InActivePromotion = [weakSelf date:now isBetweenDate:obj1.promotion.start andDate:obj1.promotion.end];
BOOL isObj2InActivePromotion = [weakSelf date:now isBetweenDate:obj2.promotion.start andDate:obj2.promotion.end];
// if they eather are in active promotion or no, just compare them by start date of the Event
if (isObj1InActivePromotion == isObj2InActivePromotion) {
return [obj1.start compare:obj2.start];
} else {
return isObj1InActivePromotion ? NSOrderedAscending : NSOrderedDescending;
}
}];
3. Additional method to work with NSDate
This method was used in sorting method
+ (BOOL)date:(NSDate *)date isBetweenDate:(NSDate *)beginDate andDate:(NSDate *)endDate
{
if ([date compare:beginDate] == NSOrderedAscending) {
return NO;
}
if ([date compare:endDate] == NSOrderedDescending) {
return NO;
}
return YES;
}
I could't check the code for obvious reasons, so sorry for any typos if they are.
I will suggest you to create "isActive" transection property in Promotion Entity to calculate active record.
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#“start” ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[fetchRequest setIncludesPropertyValues:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
After that you can filter fetch result by sorting :
NSPredicate *predicate = [NSPredicate predicateWithFormat:#“isActive == %#", #1];
NSMutableArray *finalResult = [NSMutableArray arrayWithArray:[results filteredArrayUsingPredicate:predicate]];
HTH.

Second times fetched data (with the same method) return null

I fetched the data from core data, first time is return correct result.
I want to fetch it again for update the fetch offset, I fetched with the same method but it return null
this is my code
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.managedObjectContext;
ChatInfo *chat = [ChatInfo getInstance];
fetchRequest.entity = [NSEntityDescription entityForName:#"AudioHistory" inManagedObjectContext:context];
// fectch max date first
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"(uploadedby == %# && sentto == %#) || (uploadedby == %# && sentto == %#)", chat.userid, chat.friendid, chat.friendid, chat.userid];
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"ms_createdAt" ascending:YES]];
NSFetchedResultsController *firstFetchedResult =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:context sectionNameKeyPath:nil
cacheName:nil];
NSError *error = nil;
NSArray *objects = [context executeFetchRequest:fetchRequest error:&error];
if (objects.count >= 10) {
[fetchRequest setFetchOffset:objects.count-10];
}
//self.fetchedResultsController = theFetchedResultsController;
self.fetchedResultsController = firstFetchedResult;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
what I do wrong?
You have some code when creating the FRC to check if it actually needs to be done:
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
so, if it already exists it will just be returned. So the second time you request the FRC a new fetch won't be made and no changes to the fetch request will be made.
If you need them to be then you need to destroy the existing FRC (self.fetchedResultsController = nil;) or have a separate methods which changes the fetch request.
Remember that when you change the fetch request you explicitly have to call performFetch on the FRC or the change will be ignored.
From your comment you want to scroll to load more, that shouldn't be done using the FRC. The FRC should just be observing the whole data set in Core Data. If you want to load more you should detect the request (button, pull, scroll, whatever) and trigger a new download / object creating in Core Data. The FRC will see that change and call its delegate methods as appropriate.

iOS Core data predicate for one-to-many

I have two entities as illustrate on the figure. I need to fetch a particular appointment and all the related attendees where attendee is not deleted (hasDeleted != 1). I have tried with different predicate formats. Following are two main predicate I have tried and not able to get the any of these predicates work. Could you please help me to overcome this issue?
NSPredicate *predicate1 = [NSPredicate predicateWithFormat:#"objectName = %# AND objectId = %i AND SUBQUERY(attendees, $x, $x.hasDeleted != 1).#count != 0", [self.userDefaults stringForKey:OBJECT_NAME], [[self.userDefaults objectForKey:OBJECT_ID] intValue]];
NSPredicate *predicate2 = [NSPredicate predicateWithFormat:#"objectName = %# AND objectId = %i AND (ANY attendees.hasDeleted != 1)", [self.userDefaults stringForKey:OBJECT_NAME], [[self.userDefaults objectForKey:OBJECT_ID] intValue]];
Appointment list tableview
Detail appointment view where the attendee details can be seen.
Fetch result controller code:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Attendee" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"uuid" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"appointment.objectName = %# AND appointment.objectId = %i AND hasDeleted != 1", [self.userDefaults stringForKey:OBJECT_NAME], [[self.userDefaults objectForKey:OBJECT_ID] intValue]];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
A fetch request always returns the objects from the managed object context. You cannot
fetch "modified objects", such as Appointment objects with a modified relationship to
"Attendee" which contains only the attendees with a certain property.
What you can do instead is to fetch the undeleted Attendee object instead
which are related to the given Appointment.
Create a fetch request for the Attendee entity and use the predicate
[NSPredicate predicateWithFormat:#"appointment.objectName = %# AND appointment.objectId = %i AND hasDeleted != 1", [self.userDefaults stringForKey:OBJECT_NAME], [[self.userDefaults objectForKey:OBJECT_ID] intValue]]
Update according to the new information: In your first view controller you use a
fetched results controller with a fetch request for the Appointment entity.
If you want to display only appointments that have any attendee which has not cancelled
then add the predicate
[NSPredicate predicateWithFormat:"ANY attendees.hasDeleted != 1"]
to this fetch request.
When the user taps on an appointment, you pass the select appointment to the next view
controllers. To display the attendees for this appointment, use a fetched results
controller with a fetch request for the Attendee entity, and add the predicate
[NSPredicate predicateWithFormat:#"appointment = %# AND hasDeleted != 1", selectedAppointment]

array count returned from NSFetchRequest is wrong

I'm using Core Data to cache data, here is my code to insert object:
//data array count is 15
for (NSDictionary *dictionary in dataArray)
{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"CacheData" inManagedObjectContext:context];
[request setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title LIKE '%#'",dictionary[#"title"]];
[request setPredicate:predicate];
NSArray *fetchArray = [context executeFetchRequest:request error:NULL];
if ([fetchArray count] == 0)
{
CacheData *cacheData = [NSEntityDescription insertNewObjectForEntityForName:#"CacheData" inManagedObjectContext:context];
[cacheData setTitle:dictionary[#"title"]];
[cacheData setLink:dictionary[#"link"]];
[cacheData setPublishDate:dictionary[#"pubDate"]];
NSError *insertError = nil;
if (![context save:&insertError])
{
return NO;
}
}
}
The count of dataArray is 15, so I should insert 15 items to Core Data.
But once I used NSFetchRequest to fetch items, the array count returned added 1, and become 16, and fetch items again, it added 1 to 17 again:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"CacheData" inManagedObjectContext:[[CacheDataManagement sharedInstance] managedObjectContext]];
[request setEntity:entity];
NSArray *fetchArray = [[[CacheDataManagement sharedInstance] managedObjectContext] executeFetchRequest:request error:NULL];
for (CacheData *data in fetchArray) {
NSLog(#"fetch:%#",[data title]);
}
NSLog(#"%ld",[fetchArray count]); //fetch array count is 16
Something wrong with my code ?
Update
Changed if ([fetchArray count] != 0) { … } to if ([fetchArray count] == 0) { … }
Since the problem it's not quite clear to me (I would like to have more details), I'll try to give you some hints.
First, the save should be done outside the for loop (In addition it's not correct to do a return within it).
// for in loop here
NSError *insertError = nil;
if (![context save:&insertError]) {
NSLog("%#", insertError);
return NO; // do it if the method you are running in returns a bool
}
Second, to check for duplicated you should rely on a GUID but if don't have it the predicate should look like the following.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title == %#",dictionary[#"title"]];
Third, also the execute for executeFetchRequest:error: should be replace with countForFetchRequest:error: since you don't need to return the objects but only a count. According to Apple doc, it
Returns the number of objects a given fetch request would have
returned if it had been passed to executeFetchRequest:error:.
Finally, in the for loop you are executing a request each time. My suggestion is to move execute a request before the loop and then checking for results within it. This according to Implementing Find-or-Create Efficiently. In this case, the pattern will enforce you to have a GUID for you entity.
Obviously, these are just hints. The real way to find the problem is to debug. In addition, I will perform tests starting from a fresh environment, i.e. the app has been deleted from the simulator or device.
In your code [fetchArray count] != 0 checks the inserting data is already exists. Try changing it like
if ([fetchArray count] == 0)
{
CacheData *cacheData = [NSEntityDescription insertNewObjectForEntityForName:#"CacheData" inManagedObjectContext:context];
[cacheData setTitle:dictionary[#"title"]];
[cacheData setLink:dictionary[#"link"]];
[cacheData setPublishDate:dictionary[#"pubDate"]];
NSError *insertError = nil;
if (![context save:&insertError])
{
return NO;
}
}

Resources