iOS - searching in a large NSArray is slow - ios

I'm building a customised UITableViewController that shows all the contacts in the iPhone and behaves like the ABPeoplePickerNavigationController. Meaning it also supports searching the contacts. I'm doing this with the code here.
I've implemented the search ability using Search Bar and Search Display Controller, and I followed this tutorial by appcoda.
Since my NSArray is an array of ABRecordRef my method of filterContentForSearchText: scope: is this:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSPredicate *resultPredicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
ABRecordRef person = (__bridge ABRecordRef)evaluatedObject;
NSString * fullname = [self getFullnameOfRecord:person];
NSPredicate *tmpPredicate = [NSPredicate predicateWithFormat:#"self contains[c] %#", searchText];
if ([tmpPredicate evaluateWithObject:fullname]) {
return YES;
} else {
NSLog(#"tmpPredicate didn't match");
return NO;
}
}];
searchResults = [self.allContacts filteredArrayUsingPredicate:resultPredicate];
}
The search results are fine, but since this is a very large array, it works very slowly. Is there a way I could improve the performance of this search mechanism?
Update: As #Eiko suggested, I tried replacing the inner NSPredicate with this code:
NSRange range = [fullname rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (range.length > 0) {
return YES;
} else {
return NO;
}
But it didn't improve the performance.

You should try to use the profiler to find the weakest line,
but I assume that the problem is that predicate block is evaluated for every entry every time.
I would suggest you to create your own wrapper class for ABRecordRef (let's say RecordWrapper), which will contain link to ABRecordRef with whole data and cache some frequent used and important values (e.g. fullName), you can obtain it once while loading list of contacts.
Then if you have an array of RecordWrapper* objects you can filter simply by calling
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"fullName contains[c] %#", searchText];
searchResults = [self.allContactsWrappers filteredArrayUsingPredicate:resultPredicate];
This should significantly increase filtering speed.

"very large array"? I'd guess that a contact list is rather small, typically < 1k elements.
That being said, the predicate stuff probably come at a premium, and with respect to this answer a simple enumuration might be the fastest. I suggest to test (and profile) on a real device, though.
I guess that creating predicates can be a costly operation (do they need to be compiled?), so you could reuse it, or even better, just do the little "contains check" on fullname yourself (just do a search with rangeOfString:options:), omitting the predicate alltogether.

Related

use NSPredicate to filter object

Here say I have a array of objects with two attributes:
// array of object
NSArray *objects
// object
NSString *primaryTag;
NSArray *secondaryTag;
Since what I want is when the this object contains the givenTag, it could be passed to a new array called results;
Here is my codes:
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"primaryTag == %# || secondaryTag CONTAINS[c] %#", givenTag, givenTag];
results = [objects filteredArrayUsingPredicate:resultPredicate];
It seems that the primaryTag works well, but the secondaryTag doesn't work, can someone help me out. I am not that familiar with NSPredicate filtering. Thanks in advance.
The most efficient way to do that is with a NSCompoundPredicate like so:
NSArray *subPredicates = #[tag1, tag2, tag3];
NSPredicate *compoundPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:subPredicates];
Your question is a little unclear so you might also want:
andPredicateWithSubpredicates
Depending on exactly what the nature of the result set you are looking for.
See Apple Docs here: NSCompoundPredicate Docs
i implemented the following custom class:
#interface CustomObject : NSObject
#property (copy, nonatomic) NSString *primaryTag;
#property (strong, nonatomic) NSArray *secondaryTag;
#end
and overrode it's description method for the NSLog statement to print something we understand:
- (NSString *)description {
return [NSString stringWithFormat:#"primaryTag: %#, secondaryTag: %#", _primaryTag, [_secondaryTag componentsJoinedByString:#", "]];
}
then i created some objects from the custom class and added them to an array:
NSMutableArray *objects = [NSMutableArray array];
CustomObject *obj1 = [CustomObject new];
obj1.primaryTag = #"stringToSearchFor";
obj1.secondaryTag = #[#"notTheStringToSearchFor", #"somethingElse"];
[objects addObject:obj1];
CustomObject *obj2 = [CustomObject new];
obj2.primaryTag = #"differentString";
obj2.secondaryTag = #[#"nothingWeAreLookingFor"];
[objects addObject:obj2];
CustomObject *obj3 = [CustomObject new];
obj3.primaryTag = #"anotherOne";
obj3.secondaryTag = #[#"whoCaresForThisString", #"stringToSearchFor"];
[objects addObject:obj3];
finally i created a string to search for and the predicate:
NSString *givenTag = #"stringToSearchFor";
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"primaryTag == %# || secondaryTag CONTAINS[c] %#", givenTag, givenTag];
when i log out the result i get the correct results:
NSLog(#"%#", [objects filteredArrayUsingPredicate:resultPredicate]);
logs:
(
"primaryTag: stringToSearchFor, secondaryTag: notTheStringToSearchFor, somethingElse",
"primaryTag: anotherOne, secondaryTag: whoCaresForThisString, stringToSearchFor"
)
which is obj1 and obj3. correct! if it does not work for you there's gotta be something else wrong with your code...
If my understanding of the original question is incorrect, please let me know, and I will adjust my answer.
Problem: You have an array of objects with 2 properties. One is primaryTag, which is a string. The second is an array of secondaryTags, which is a collection of strings. You want to filter all objects where either the primaryTag matches, or where the search string matches one of the secondaryTags.
Answer The proper way to match strings is via MATCHES or CONTAINS.
NSPredicate *pPredicate =
[NSPredicate predicateWithFormat:#"%K CONTAINS[cd] %#",
#"primaryTag", searchString];
NSPredicate *sPredicate =
[NSPredicate
predicateWithFormat:#"SUBQUERY(%K, $st, $st CONTAINS[cd] %#).#count > 0",
#"secondaryTags", searchString];
NSCompoundPredicate *searchPredicate =
[NSCompoundPredicate orPredicateWithSubPredicates:#[ pPredicate, sPredicate ]];
How it works: The first predicate is a straightforward match. You can replace CONTAINS with MATCHES, if that better fits the kind of comparison you wish to make. The [cd] suffix means case-insensitive and diacritic-insensitive. It's normal to include those when searching/filtering, but again, it's up to you. Instead of embedding the property name in the predicate format string, I use %K and a replacement parameter. In production code, that replacement parameter would be a constant.
The second predicate is a little trickier. It uses a SUBQUERY() to filter the secondaryTags array, and returns the object as matching if at least one secondary tag matches the search string. SUBQUERY() is a function with 3 parameters. The first is the collection being searched. The second is a temporary variable that represents each item in the collection, in turn; it is used in the 3rd parameter. The 3rd parameter is a regular predicate. Each item in the collection that matches the filter is included in the output of SUBQUERY(). At the end, the matching secondary tags are counted (via #count), and if the count is greater than zero, the original object is considered to have matched, so will be included in the filtered output.
Finally, we combine these two predicates into one searchPredicate, which can now be used to filter your array of objects.
I seen this issue,
My normal approch is to use the NSPredicate twice,
So that I can track the result at every steps:
Option 1:
NSPredicate *resultPredicate1 = [NSPredicate predicateWithFormat:#"primaryTag == %#", givenTag];
results1 = [objects filteredArrayUsingPredicate:resultPredicate1];
NSPredicate *resultPredicate2 = [NSPredicate predicateWithFormat:#"secondaryTag CONTAINS[c] %#", givenTag];
finalResults = [results1 filteredArrayUsingPredicate:resultPredicate2];
Option 2:
Use NSCompoundPredicate to compound multiple filtering. You can easily find many examples on google and stackOverFlow.
Hope this will help,
Thanks

Get all dictionary values for certain keys (in an array)

I'm trying to get all the values of keys that are only in an array after filtering
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
if ([scope isEqualToString:#"Calendar"]) {
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"SELF contains[c] %#", searchText];
self.searchResults = [NSMutableArray arrayWithArray:[[allsearchDictKeys filteredArrayUsingPredicate:resultPredicate] sortedArrayUsingSelector:#selector(localizedStandardCompare:)]];
}
}
How can I get an array, that has the values for the keys in self.searchResults array only, with the same indexPaths as their counterpart?
This is being passed into my UISearchResults updating tableView, so I would like this to occur prior to the update so I don't have to do much in cellForRowAtIndexPath. I know I can enumerate through them, and add those objects to another array, but my question is can I bypass that and just simply directly add them to an array?
Example:
self.searchResultsValuesArray = [searchDict valuesForKeys:self.searchResults];
I know there is no valuesForKeys but it's there for illustrative purposes.
You can use the NSDictionaryMethod objectsForKeys:notFoundMarker:
self.searchResultsValuesArray = [searchDict objectsForKeys:self.searchResults notFoundMarker:#""];

Filter nsmutable array by CGRectIntersectsRect

I wanna extract from an array of SKSpriteNode only the elements that intersect with a predeterminated frame. I can do it by a for iteration:
for (SKSpriteNode* Object in Array) {
if (CGRectIntersectsRect(Frame,Object.frame)) {
//extraction code
}
}
However performace of this method seems to be poor,there's a way to do this operation in a faster way? I have tried something like this:
NSPredicate *Predicate = [NSPredicate predicateWithFormat:#"CGRectIntersectsRect(Frame,SELF.frame)"];
NSArray *Results = [Array filteredArrayUsingPredicate:Predicate];
But this create the error "Unable to parse function name 'CGRectIntersectsRect' into supported selector (CGRectIntersectsRect)". What's wrong? Using a predicate instead a for will give me some gain in performance?
Since predicate parser cannot recognize a free-standing C function, you can make a predicate from a block:
NSPredicate *intersects = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
return CGRectIntersectsRect(Frame, obj.frame);
}];
NSArray *results = [Array filteredArrayUsingPredicate:intersects];
I am not sure about the performance gain in comparison to the loop, though, because the number of comparisons is going to remain unchanged.

Lazy instantiation before or inside of a loop

I'm trying to implement lazy instantiation within my code to speed up my app. My question is, when dealing with a for loop, should I create an object right before the loop begins, or within the loop, basically should I do this:
NSPredicate *pred= [NSPredicate predicateWithFormat:#"self LIKE %#", filter];
for(NSString* str in myArray){
//do stuff
}
or this:
for(NSString* str in myArray){
//do stuff
NSPredicate *pred= [NSPredicate predicateWithFormat:#"self LIKE %#", filter];
//do stuff that needs this variable
}
My thoughts were to do the first one, but a friend says the 2nd one, although isn't the 2nd one creating the object everytime you go through the loop so it's more costly?
Neither one of these is truly lazy:
The first one creates the predicate even when the loop does not need to execute at all
The second one is optimal only when the loop executes once; otherwise, throw-away instances of NSPredicate get created.
If you wish to stay truly lazy, you could add a condition around the first code example to check that myArray is not empty. You could also use a conditional expression, like this:
NSPredicate *pred= myArray.count ? [NSPredicate predicateWithFormat:#"self LIKE %#", filter] : nil;
This is not particularly readable, though, so I would recommend staying with your first approach, which is optimal when the loop is entered at least once.
Well, lazy instantiation would look something like this I think...
NSPredicate *pred = nil;
for (NSString *string in myArray) {
// do stuff...
// if predicate is required
if (!pred) {
pred = [NSPredicate predicateWithFormat:#"self LIKE %#", filter];
}
// use pred
}
But out of the three options. I'd use your first method.
I don't see much of a choice, with the first way being both clean and simple. The equivalent alternative is actually:
NSPredicate *pred = nil;
for (NSString* str in myArray){
if (!pred)
pred = [NSPredicate predicateWithFormat:#"self LIKE %#", filter];
//do stuff
}
Which is clearly more complicated than it needs to be.
I would also argue that this isn't really lazy initialization, which really relates to initialization of instance variables at some point after object creation.

NSPredicate possibly matching multiple properties

I have a Car class with instance variables such as "color" and "make".
I need a NSPredicate that allows me to search for either color or make or both.
NSPredicate*predicate=[NSPredicate predicateWithFormat:#"(color contains[c] %#) AND (make contains[c] %#)", chosenColor,chosenMake];
This predicate requires that there is BOTH a color and a make. If the user only gives a color, no results will be returned because the make then will be "nil". No cars has nil for any instance variables.
Also, the search will be for many variables, not only color and make, so an if/case situation is not wanted. Is there any options that gives my the possibility to search for "AND if NOT nil". I will appreciate any help.
You can build your predicate format dynamically to test only non-nil attributes. More on that here. Also consider making your search diacritic-insensitive (adding a 'd' to your CONTAINS statement). Take "Škoda" for example. You want people to find it with "skoda" as well.
This is pretty easy with the NSCompoundPredicate API:
NSString *chosenColor = ...;
NSString *chosenMake = ...;
NSMutableArray *subpredicates = [NSMutableArray array];
if (chosenColor != nil) {
NSPredicate *p = [NSPredicate predicateWithFormat:#"color contains[cd] %#", chosenColor];
[subpredicates addObject:p];
}
if (chosenMake != nil) {
NSPredicate *p = [NSPredicate predicateWithFormat:#"make contains[cd] %#", chosenMake];
[subpredicates addObject:p];
}
NSPredicate *final = nil;
if ([subpredicates count] > 0) {
final = [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];
}

Resources