App becomes unresponsive on NSFetchRequests - iphone

I have the above CoreData Model in my first iPad app. I'm building a filtering system in a TableViewController as shown below. The problem is that whenever I make a UI change, toggle a switch of tap a button, My UI becomes non-responsive for a second or two. I run a really long function that recreates the fetch request for the photos, then runs more count fetches to determine whether the control should be enabled. I just don't know how I can break this apart in a meaningful way that would prevent the hang. Even If I need to add a spinning view for a second or so, I'm happy with that. Just want to get rid of the lag.
As I mentioned, this is my first attempt at iOS development so I would appreciate any suggestions...
-(void) refilterPhotos {
/*
* First section builds the NSCompoundPredicate to use for searching my CoreData Photo objects.
Second section runs queries so 0 result controls can be disabled.
*/
subpredicates = [[NSMutableArray alloc] init];
NSPredicate *isNewPredicate;
if(newSwitch.on) {
isNewPredicate = [NSPredicate predicateWithFormat:#"is_new == 1"];
} else {
isNewPredicate = [NSPredicate predicateWithFormat:#"is_new == 0"];
}
[subpredicates addObject:isNewPredicate];
//Photo Types
PhotoType *photoType;
NSPredicate *photoTypePredicate;
for (UISwitch *photoSwitch in photoSwitches) {
PhotoType * type = (PhotoType *) photoSwitch.property;
if([type.selected boolValue] == YES) {
NSLog(#"photo_type.label == %#", type.label);
photoType = type;
photoTypePredicate = [NSPredicate predicateWithFormat:#"photo_type.label == %#", type.label];
break;
}
}
//Feed Types
FeedType *feedType;
NSPredicate *feedTypePredicate;
for (UISwitch *feedSwitch in feedSwitches) {
FeedType * type = (FeedType *) feedSwitch.property;
if([type.selected boolValue] == YES) {
NSLog(#"feed_type.label == %#", type.label);
feedType = type;
feedTypePredicate = [NSPredicate predicateWithFormat:#"feed_type.label == %#", type.label];
break;
}
}
//Markets
NSArray *filteredMarkets = [model.availableMarkets filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"selected == 1"]];
for (Market *market in filteredMarkets) {
[subpredicates addObject:[NSPredicate predicateWithFormat:#"ANY markets.name == %#", market.name]];
}
//Tags
NSArray *filteredTags = [model.availableTags filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"selected == 1"]];
for (Tag *tag in filteredTags) {
NSLog(#"ANY tags.name == %#",tag.name);
[subpredicates addObject:[NSPredicate predicateWithFormat:#"ANY tags.name == %#", tag.name]];
}
if(photoTypePredicate)
[subpredicates addObject:photoTypePredicate];
if(feedTypePredicate)
[subpredicates addObject:feedTypePredicate];
NSPredicate *finished = [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];//Your final predicate
model.availablePhotos = [model fetchPhotoswithPredicate:finished];
[[self parentViewController] setTitle:[NSString stringWithFormat:#"%d items",[model.availablePhotos count]]];
NSLog(#"FILTERED PHOTOS:::: %d", [model.availablePhotos count]);
[gridVC reloadGrid];
/**
* Filtering Section Here, I'm running count requests for each grouping of controls to ensure if they're selected, results will be returned.
* If zero results, I'll disable that control. For the switch-based controls, I need to removed them before running my fetches since there can only be
* one switch value per photo.
*/
//Have to remove the existing type predicate since they're exlcusive values
[subpredicates removeObject:isNewPredicate];
//New Toggle
NSPredicate *newRemainderPredicate = [NSPredicate predicateWithFormat:#"is_new == %d",newSwitch.on?0:1];
[subpredicates addObject:newRemainderPredicate];
if([model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]]<1) {
[newSwitch setEnabled:NO];
} else {
[newSwitch setEnabled:YES];
}
[subpredicates removeObject:newRemainderPredicate];
[subpredicates addObject:isNewPredicate];
[subpredicates removeObject:photoTypePredicate];
//Photo Type Toggles
NSArray *remainderPhotoTypes = [photoSwitches filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"on == NO"]];
for( UISwitch*control in remainderPhotoTypes) {
PhotoType *remainderPhotoType = (PhotoType*)control.property;
[subpredicates addObject:[NSPredicate predicateWithFormat:#"photo_type == %#", remainderPhotoType]];
if([model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]]<1) {
//NSLog(#"PHOTOTYPE OFF %#", remainderPhotoType.label);
control.enabled = NO;
} else {
//NSLog(#"PHOTOTYPE ON %# count = %d", remainderPhotoType.label, [[model fetchPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]] count]);
control.enabled = YES;
}
remainderPhotoType.enabled = [NSNumber numberWithBool:control.enabled];
[subpredicates removeObject:[NSPredicate predicateWithFormat:#"photo_type == %#", remainderPhotoType]];
}
if(photoTypePredicate)
[subpredicates addObject:photoTypePredicate];
[subpredicates removeObject:feedTypePredicate];
//Feed Type Toggles
NSArray *remainderFeedTypes = [feedSwitches filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"on == NO"]];
for( UISwitch*control in remainderFeedTypes) {
PhotoType *remainderFeedType = (PhotoType*)control.property;
[subpredicates addObject:[NSPredicate predicateWithFormat:#"feed_type == %#", remainderFeedType]];
if([model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]]<1) {
control.enabled = NO;
} else {
control.enabled = YES;
}
remainderFeedType.enabled = [NSNumber numberWithBool:control.enabled];
[subpredicates removeObject:[NSPredicate predicateWithFormat:#"feed_type == %#", remainderFeedType]];
}
if(feedTypePredicate)
[subpredicates addObject:feedTypePredicate];
NSArray *remainderMarkets = [[model availableMarkets] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"selected == 0"]];
//Markets..many-to-many so I don't remove the existing predicate
for( Market *remainderMarket in remainderMarkets) {
[subpredicates addObject:[NSPredicate predicateWithFormat:#"ANY markets == %#", remainderMarket]];
NSInteger countForTag = [model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]];
if(countForTag<1) {
remainderMarket.enabled = [NSNumber numberWithInt:0];
} else {
remainderMarket.enabled = [NSNumber numberWithInt:1];
}
[subpredicates removeObject:[NSPredicate predicateWithFormat:#"ANY markets == %#", remainderMarket]];
}
NSArray *remainderTags = [[model availableTags] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"selected == 0"]];
//TAGS..many-to-many so I don't remove the existing predicate
int tagCounter = 0;
for( Tag *remainderTag in remainderTags) {
[subpredicates addObject:[NSPredicate predicateWithFormat:#"ANY tags == %#", remainderTag]];
NSInteger countForTag = [model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]];
if(countForTag<1) {
NSLog(#"TAG OFF %#", remainderTag.name);
remainderTag.enabled = 0;
} else {
NSLog(#"TAG ON %# count = %d", remainderTag.name, countForTag);
remainderTag.enabled = [NSNumber numberWithInt:1];
}
[subpredicates removeObject:[NSPredicate predicateWithFormat:#"ANY tags.name == %#", remainderTag.name]];
tagCounter ++;
}
//Update the controls with this new data
[self.tableView reloadData];
}

OK, there are several things to consider here.
First, I would consider creating indexes for the main search fields. Without an index, each search is linear, because it has to check the value of each record. An index will result in much faster searching times.
Second, I'd be very careful about ordering in a compound predicate. It will filter them based on order. Thus, you want to make you fastest, most filtering predicates first. Trim the possible solution space a quickly as possible.
You can gain a lot by indexing the attributes you use in the first 1-3 predicates. I note at the bottom, when you query for counts, you are still using the same compound predicate. Do you really want that? Also, in this code
//Have to remove the existing type predicate since they're exlcusive values
[subpredicates removeObject:isNewPredicate];
//New Toggle
NSPredicate *newRemainderPredicate = [NSPredicate predicateWithFormat:#"is_new == %d",newSwitch.on?0:1];
[subpredicates addObject:newRemainderPredicate];
You are removing the is_new check from the front, and placing it at the rear. If you are just checking this one predicate to toggle that switch, and you only care to see if there are 0 or more, why even use the entire compound predicate? Is the "toggle" going to be on/off relative to all the other fields?
If you continue with this, remember, it is going to do all those other predicates first (and some are references). Try to keep them in a good order to filter as much as possible, as quickly as you can.
Third, using references is convenient, but expensive. You can possible get better performance by querying those separately, and then using the compound predicate to filter the in-memory objects.
Fourth, you should be executing all these queries in a separate thread. That is very easy to do, but the exact method depends on your current ManagedObjecttContext arrangement. Do you have a single MOC, a parent/child relationship, a UIManagedDocument? Basically, you can create a separate MOC, and call performBlock to execute the fetches. In fact, you can fire all those fetches off asynchronously at the same time with multiple MOCs.
Then, you can just call into the main thread when they are done.
Finally, you may want to consider denormalizing your database. It will cause you to use more space, but fetches will be much faster. Specifically, the relationship fields... you could put the photo/feed labels in with the Photo itself. That way, when searching, you don't have to do the extra join to get those records.
So, it's not a simple answer, but implement each of these, and see if your performance does not improve considerably (not to mention your UI responsiveness).

Related

Optimizing this Core Data request

I have an entity in Core Data named MusicInterest. I have to add 5000 or so of these at a time and my current process is to query to see if the MusicInterest exists already, if not create a new one.
It seems this requires 5000 trips to the store to see if each title exists. There are also, of course, insert trips, but the 5000 queries is what's slowing me down.
Each FacebookFriend will have multiple music interests, and I enumerate through each one using an array of string titles, calling the following code.
Any ideas how to optimize this?
+ (MusicInterest*) musicInterestForFacebookFriend:(FacebookFriend*)facebookFriend WithTitle:(NSString*)musicTitle UsingManagedObjectContext:(NSManagedObjectContext*)moc
{
// query to see if there
NSArray *matches = [self queryForMusicTitle:musicTitle moc:moc];
if (([matches count] >= 1)) {
// NSLog(#"Music already in database");
MusicInterest *existingMusic = [matches lastObject];
[existingMusic addLikedByObject:facebookFriend];
return [matches lastObject];
} else {
// create new Music Interest
MusicInterest *newMusic = [NSEntityDescription insertNewObjectForEntityForName:#"MusicInterest" inManagedObjectContext:moc];
newMusic.title = musicTitle;
[newMusic addLikedByObject:facebookFriend];
return newMusic;
}
}
+ (NSArray *)queryForMusicTitle:(NSString *)MusicTitle moc:(NSManagedObjectContext *)moc
{
// query to see if there
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MusicInterest"];
request.predicate = [NSPredicate predicateWithFormat:#"title == %#", [NSString stringWithFormat:#"%#", MusicTitle]];
NSError *error = nil;
NSArray *matches = [moc executeFetchRequest:request error:&error];
if (error) {
NSLog(#"Error querying title in Music interest. Error = %#", error);
}
return matches;
}
UPDATE:
I employed the design suggested in the Core Data programming guide and it reduced my time from 12 seconds to 4 seconds (still needs some optimization in other areas :)
The guide only includes half the sample code - I thought I would share my complete implementation:
musicArray = [[music componentsSeparatedByString:#", "] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if (obj1 > obj2)
return NSOrderedDescending;
else if (obj1 < obj2)
return NSOrderedAscending;
return NSOrderedSame;
}];
if (musicArray) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MusicInterest"];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"title IN %#", musicArray]];
[fetchRequest setSortDescriptors:
#[[[NSSortDescriptor alloc] initWithKey: #"title" ascending:YES]]];
NSError *fetchError = nil;
NSArray *musicInterestMatchingTitles = [backgroundContext executeFetchRequest:fetchRequest error:&fetchError];
if ([musicArray count] > 0) {
// walk musicArray and musicInterestsMatchingTitles in parallel
for (int i = 0; i < [musicArray count]; i++) {
NSString *title = musicArray[i];
if (i < [musicInterestMatchingTitles count]) {
MusicInterest *comparingMusicInterest = musicInterestMatchingTitles[i];
// compare each title
if (![title isEqualToString:comparingMusicInterest.title]) {
// if it doesn't exist as a ManagedObject (a MusicInterest), create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
} else {
// otherwise, just establish the relationship
[comparingMusicInterest addLikedByObject:friend];
}
} else {
// if there are no existing matching managedObjects, create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
}
}
}
}
}];
[self saveBackgroundContext:backgroundContext];
Implementing Find-or-Create Efficiently in the "Core Data Programming Guide" describes a pattern that might be useful here. The basic idea is:
Sort your list of items that you want to insert/update by some unique id that is also stored in
the database.
Perform a single fetch request that fetches all objects from the database that have an id from your list, sorted by the same id.
Now traverse your list and the array of fetched items in parallel, to find which items have to be inserted and which items already exist and can be updated.

How to sort GKTurnBasedMatch by most recently active?

I am creating a simple word game with a menu screen in which I am displaying all of the user's active matches. I would like to sort this array of matches in order from most recently to least recently active, but the only timestamp property associated with players taking turns is a property of GKTurnBasedParticipant...GKTurnBasedMatch has no useful sorting property.
GKTurnBasedMatch has an array of GKTurnBasedParticipant objects as a property, so I would certainly be able to come up with some sort of solution, but I can't think of anything that wouldn't be really messy and inefficient. Is there any way something simple like NSPredicate could be used in a case like this to drill down into each array of participants, look at the latest timestamp and sort all the matches in one go?
I don't have an NSPredicate-based solution, or probably anything as elegant as you had hoped, but I ran into the same issue and wrote my own solution and it wasn't actually that bad.
My solution is for a game that can only have two participants, so modify accordingly, but here is the code I ended up using:
[myGamesArray sortUsingComparator:^NSComparisonResult(CHGame *game1,
CHGame *game2) {
if (YES == [game1 localPlayersTurn] && NO == [game2 localPlayersTurn]) {
return NSOrderedAscending;
} else if (NO == [game1 localPlayersTurn] && YES == [game2 localPlayersTurn]) {
return NSOrderedDescending;
}
NSDate *lm1 = [game1.match lastMove];
NSDate *lm2 = [game2.match lastMove];
if (lm1 != nil && lm2 != nil) {
return [lm1 compare:lm2];
}
return NSOrderedSame;
}];
where CHGame is a custom class I built for my games (which have a GKTurnBasedMatch match property), and the instance method localPlayersTurn returns a BOOL indicating whether or not it is the local participant's turn or not.
And then I wrote a lastMove method in a category on GKTurnBasedMatch:
- (NSDate *)lastMove {
GKTurnBasedParticipant *localParticipant, *otherParticipant;
NSDate *lastMove;
for (GKTurnBasedParticipant *participant in self.participants) {
if (YES == [participant.playerID isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
localParticipant = participant;
} else {
otherParticipant = participant;
}
}
if (localParticipant == self.currentParticipant) {
lastMove = otherParticipant.lastTurnDate;
} else {
lastMove = localParticipant.lastTurnDate;
}
return lastMove;
}
Again, this only works for two total participants, but would be easy to modify for any number of them.
Hope this helps even though it's not exactly what you asked for.
Sort turn-based matches by last turn of current participant
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error)
{
NSString *descriptorKey = #"currentParticipant.lastTurnDate";
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:descriptorKey
ascending:NO];
NSArray *sortedMatches = [matches sortedArrayUsingDescriptors:#[sortDescriptor]];
}];
Sort turn-based matches by date created
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error)
{
NSString *descriptorKey = #"creationDate";
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:descriptorKey
ascending:NO];
NSArray *sortedMatches = [matches sortedArrayUsingDescriptors:#[sortDescriptor]];
}];

NSPredicate to compare integer using Contains

I am using a NSPredicate to search numbers in the list using UISearchBar ,
it works in case of strings but does not work for an integer
I am using the following predicate
predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"%# contains[c] %d", #"number", [searchBar.text intValue]]];
[objectArray filterUsingPredicate:predicate];
[tableview reloadData];
FOR example if I type 1 then all the ones in the array must be listed, I have tried == it works only for the exact number if tried any work around for this any body?
Now I get an error if I use this method "Can't use in/contains operator with collection"
I think this predicate should work for you:
predicate = [NSPredicate predicateWithFormat:#"self.number.stringValue CONTAINS %#",searchBar.text];
After thinking about this, I'm not sure why self.number.stringValue works, but it did when I tested it (self.number is an int). Not sure why I can send stringValue to an int?
Predicates can be tricky to work with, so perhaps an alternative would work for you:
NSInteger index = 0;
while (index < objectArray.count)
{
NSString *currentString = [objectArray objectAtIndex:index];
if ([currentString rangeOfString:searchBar.text].length == 0)
{
[objectArray removeObjectAtIndex:index];
continue;
}
index++;
}
Here, any strings in your array that do not contain your searchBar text will be removed.

SearchDisplayController search multiple arrays

Currently I'm populating my tableviewcells with the contents of multiple arrays representing a name, id, etc.
My question comes when I start to use the search display controller. I have an array with a list of names, a list of IDs, a list of barcodes, and a list of Aliases. When the user types in the search bar I need to be able to search all 4 arrays. When it finds the result in 1 array it has to pair the result with the 3 other arrays..
Example
Names (apple,carrot,banana, dog)
alias (red, orange, yellow, brown)
barcode (1,2,10,20)
id (30, 40, 50, 60)
So if the user types "a" I should populate the table view with
Apple, Carrot, Banana and the associated alias, barcode, id.
If the user were to type 2 I should only get
carrot and dog.
If the user were to type 0 I would get all of those items.
Any ideas how to accomplish this?
UPDATE:
This is how I did it.
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
BOOL shouldReturn = FALSE;
[searchResults removeAllObjects];
for (int i = 0; i < [itemIDRows count]; i++) {
BOOL foundResult = FALSE;
if ([[itemIDRows objectAtIndex:i] rangeOfString:searchString].location != NSNotFound) {
foundResult = TRUE;
}
if ([[nameRows objectAtIndex:i] rangeOfString:searchString].location != NSNotFound) {
foundResult = TRUE;
}
if ([[barcodeRows objectAtIndex:i] rangeOfString:searchString].location != NSNotFound) {
foundResult = TRUE;
}
if ([[aliasRows objectAtIndex:i] rangeOfString:searchString].location != NSNotFound) {
foundResult = TRUE;
}
if (foundResult) {
NSNumber *result = [NSNumber numberWithInt:i];
if ([self searchResults] == nil) {
NSMutableArray *array = [[NSMutableArray alloc] init];
[self setSearchResults:array];
[array release];
}
[searchResults addObject:result];
shouldReturn = YES;
}
}
return shouldReturn;
}
Then when I'm populating the tableview I do something like this
if ([tableView isEqual:self.searchDisplayController.searchResultsTableView]) {
[cell setCellContentsName:[NSString stringWithFormat:#"%#", [nameRows objectAtIndex:[[searchResults objectAtIndex:indexPath.row] integerValue]]];
} else {
[cell setCellContentsName:[NSString stringWithFormat:#"%#", [nameRows objectAtIndex:indexPath.row]];
}
However when I type something like 9999 it brings up instances where only 1 9 is in the ID or barcode. Any ideas how to fix that?
UPDATE2:
Solved the problem by having the list always refresh instead of only reloading the data if a result was found. Now it works perfectly :D
The search display controller calls the
UISearchDisplayDelegate
method:
searchDisplayController:shouldReloadTableForSearchString:
Inside this method, you need to implement your logic. This logic will need to search all 4 of your arrays for hits, and do the appropriate lookups (i.e. to get from orange to carrot, or from 50 to banana). Each time you get a hit, I would put it in an NSMutableSet (to prevent dupes). Then when you're done searching all arrays, copy the set into the array that your table's data source reads from.
If you want to show the user WHY a given row is a hit (i.e. they typed 50 and got banana), you'd have to display all 4 of the attributes in your table cell. And you'd need to highlight the part that matched. If you do this, I'd create a small container class, something like "searchHit" that contains all 4 attributes, as well as a flag for which attribute got the hit, and possibly the substring of the attribute that got the hit (so you can use a yellow background for this substring, for example.) The tableView's data source would then have an array of these searchHit objects to display, and your cellForRowAtIndexPath would need to decode this object and display the hit appropriately.
You can do that with NSPredicate using KVC object.
Create an NSObject respond to the KVC scheme http://theocacao.com/document.page/161 . You can use property for that.
Filter your array with an NSPredicate http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSPredicate_Class/Reference/NSPredicate.html
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"self.name LIKE[cd] %# OR self.alias LIKE[cd] %#",searchString,searchString];
NSArray *result = [baseArray filteredArrayUsingPredicate:predicate];

Word boundaries (\b) in NSPredicate causing NSFetchRequest to return no managed objects

Using Core Data w/a sqlite store on iPhone.... I've got a bunch of comic book image entities, each with a string that includes the comic's issue#, e.g.: image.imageTitle = #"Issue 12: Special Edition";
Part of the UI allows the user to type in an issue number to jump to the next issue. My initial code for this was sloooooooow because imageAtIndex: queries Core Data for one object at a time. Over several hundred issues, it could take upwards of 40 seconds just to get through the first loop!
Slow Code:
// Seek forward from the next page to the right
for (i = currentPage + 1; i < [self numberOfPages]; i++) {
iterationString = [[self imageAtIndex:i] imageTitle];
iterationNumber = [[iterationString stringByTrimmingCharactersInSet:nonDigits] intValue];
if (issueNumber == iterationNumber) {
keepLooking = NO;
break;
}
}
// If nothing was found to the right, seek forward from 0 to the current page
if (i == [self numberOfPages] && keepLooking) {
for (i = 0 ; i < currentPage; i++) {
iterationString = [[self imageAtIndex:i] imageTitle];
iterationNumber = [[iterationString stringByTrimmingCharactersInSet:nonDigits] intValue];
if (issueNumber == iterationNumber) {
keepLooking = NO;
break;
}
}
}
Hoping for a much more efficient solution, I decided to try making a direct query on Core Data like so:
NSString *issueNumber = #"12";
NSString *issueWithWordBoundaries = [NSString stringWithFormat:#"\\b%#\\b",issueNumber];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(groupID == %#) AND (imageTitle CONTAINS %#)", groupID, issueWithWordBoundaries];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"CBImage" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:predicate];
[fetchRequest setIncludesSubentities:NO]; // Not sure if this is needed, but just in case....
// Execute the fetch
NSError *error = nil;
NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
// [fetchedImages count] == 0
Between the Predicate Programming Guide and the ICU regex specs, I figured the \b's would help prevent a search for 12 returning 120, 121, 122, etc. Instead, it doesn't return anything from the store at all!
On the other hand, if I leave off the word boundaries and search instead for stringWithFormat:#"%#",issueNumber, I get dozens of managed objects returned, from 12 to 129 to 412.
My best guess at this point is that I've run into one of Core Data's Constraints and Limitations. If not, what am I doing wrong? If so, is there a workaround that offers both an exact match and the speed of a single fetch?
\b refers to a backspace character. Try
[NSString stringWithFormat:#"\\b%#\\b",issueNumber];
// ^^ ^^
And to perform RegEx match, use MATCHES, not CONTAINS.
Turns out the word boundaries were a red herring: My problem was a variant on this issue: The regex couldn't succeed without some wild cards to match the bits I didn't care about in the imageTitles.
The general case solution to finding an exact phrase using NSPredicate is therefore:
NSString *exactPhrase = #"phrase_you_hope_to_find_in_another_string";
NSString *regularExpression = [NSString stringWithFormat:#".*\\b%#\\b.*",exactPhrase];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"stringToSearch CONTAINS %#", exactPhrase];
// Assuming a string or an entity's string attribute named stringToSearch