NSPredicate with block - how does it work - ios

I have a search mechanism in my application and thus far an object of NSPredicate is used to filter objects.
I'm pretty confused because of the magic happening in the runtime of my program. In my program there is an array with names and another mutable array with filtered names declared as properties.
#property (strong, nonatomic) NSArray *names;
#property (strong, nonatomic) NSMutableArray *filteredNames;
I initialize the array with names when the view is loaded.
self.names = #[
#"Aeron",
#"Brandon",
#"Chris",
#"David",
#"Elvis",
#"Francisco",
#"George",
#"Oliver",
#"Lary",
#"Neythan",
#"Marcus",
#"Phil"
];
Then I setup a table view and its content, install search mechanism (with iOS 8's one, i.e. UISearchController). And here we go, I implement the UISearchResultsUpdating protocol and my updateSearchResultsForSearchController: is presented below.
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *input = searchController.searchBar.text;
if (input.length > 0) {
[self.filteredNames removeAllObjects];
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
NSLog(#"Got [%#] string.", evaluatedObject);
NSRange range = [evaluatedObject rangeOfString:input options:NSCaseInsensitiveSearch];
return range.location != NSNotFound;
}];
NSArray *matches = [self.names filteredArrayUsingPredicate:predicate];
[self.filteredNames addObjectsFromArray:matches];
[((UITableViewController *)self.searchController.searchResultsController).tableView reloadData];
}
}
So it works perfectly. But I wonder, why do I get an iteration of my array with names inside of the block? I know that blocks are good due to their specification of saving their frame from a stack with all variables and certain values for the moment in past, therefore they can be executed lately. But why does the block iterate this particular array? I tried to declare a new one and initialize it (since I thought it took all collections and their values), but it didn't work.
Thank you in advance!

To filter the array, you have to iterate through every element and test it against the predicate block. I believe the behaviour you're experiencing is intended.

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

Warning at method definition

Following method is showing me a warning, but the app is executing as expected. Please could you check the code and tell me what is wrong there? Only if this important to the app, if the warning is not dangering the app, then tell me if I could let this as it is...thank you
The warning is : Incompatible pointer types assigning to 'NSMutableArray *' from 'NSArray *' at the method definition.
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
self.searchResults = [[self.fetchedResultsController fetchedObjects] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
{
ToDoItem * item = evaluatedObject;
NSString* name = item.todoName;
//searchText having length < 3 should not be considered
if (!!searchText && [searchText length] < 3) {
return YES;
}
if ([scope isEqualToString:#"All"] || [name isEqualToString:scope]) {
return ([name rangeOfString:searchText].location != NSNotFound);
}
return NO; //if nothing matches
}]];
}
self.searchResults = [[[self.fetchedResultsController fetchedObjects]
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:
^BOOL(id evaluatedObject, NSDictionary *bindings) mutableCopy];
mutableCopy is a method on many objects for which a mutable and immutable version exists. In the case of things like NSArray, NSString, NSData, etc, calling mutableCopy on one of these instances will return a mutable version containing the same contents as the original object you called the method on.
For example,
NSArray *immutableArray = [NSArray arrayWithObjects:#"foo",#"bar"];
NSMutableArray *mutableArray = [immutableArray mutableCopy];
However, if you don't intend for searchResults to be an NSMutableArray, you should change it's declaration:
#property (nonatomic,strong) NSArray *searchResults
If you don't intend it to be mutable, it should be declared as immutable.
Given your claim that the warning is not effecting the performance of your app, my best guess is that the proper solution would be changing searchResults from NSMutableArray to NSArray.
filteredArrayUsingPredicate returns an immutable NSArray,
and you seem to have declared searchResults as NSMutableArray.
So either
change the declaration of searchResults to NSArray, or
make a mutableCopy before assigning it.
The proper solution depends on whether you need to modify the searchResults later or not.

How to use one array as predicate for another array?

NSArray *arrClient = [[NSArray alloc] initWithObjects:#"record 1", #"record 2", nil];
NSArray *arrServer = [[NSArray alloc] initWithObjects:#"record 1", #"record 3", nil];
On arrServer I would like to apply predicates to filter only those entries that DON'T already exist in arrClient. e.g. in this case record 1 exist in both arrays and shall be ignored, hence only an array with one entry with the "record 3" string shall be returned.
Is this possible?
UPDATE
The answers below are great. I believe I need to give a real example to verify if what I am doing makes sense after all. (I am still giving a compact version below)
Now the clientItems will be of type FTRecord (Core Data)
#interface FTRecord : NSManagedObject
...
#property (nonatomic) NSTimeInterval recordDate;
#end
#implementation FTRecord
...
#dynamic recordDate;
#end
This class below is a holder for parsing json from a REST service. Hence the serverItems we mentioned earlier will be of this type.
#interface FTjsonRecord : NSObject <JSONSerializable>
{
}
#property (nonatomic) NSDate *recordDate;
#implementation FTjsonRecord
- (NSUInteger)hash
{
return [[self recordDate] hash];
}
- (BOOL)isEqual:(id)object
{
if ([object isKindOfClass:[FTjsonRecord self]]) {
FTjsonRecord *other = object;
return [[self recordDate] isEqualToDate:[other recordDate]];
}
else if ([object isKindOfClass:[FTRecord self]]) {
FTRecord *other = object;
return [[self recordDate] isEqualToDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[other recordDate]]];
}
else {
return NO;
}
}
Going with Wain's example, this seems to work fine. Now is this feasible?
Keep in mind that serverItems are just temporary and only used for syncing with server, and will be thrown away. clientItems is the one that remains in place.
UPDATE 2:
This time I am trying Manu's solution:
I have created this method on my Client DBStore, which is called by the predicate.
The reason I can't use containsObject is because the class types in serverItems and clientItems are not the same type.
-(BOOL)recordExistsForDate:(NSDate *)date
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"recordDate == %#", date];
NSArray *arr = [allRecords filteredArrayUsingPredicate:predicate];
if (arr && [arr count] > 0) {
return YES;
} else {
return NO;
}
}
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(FTjsonRecord *evaluatedObject, NSDictionary *bindings) {
return ![store recordExistsForDate:[evaluatedObject recordDate]];
}];
NSSet *set = [[serverRecords items] filteredSetUsingPredicate:predicate];
What worries me about this solution though, is the linear read from my clientItems (allRecords). I am not sure how efficient it is using the predicate on the array, wonder if there is a better way to achieve this.
You can use NSSet to get the union, intersection and difference (minus) with other sets. This more accurately matches what you're trying to do.
NSMutableSet *serverItems = [[NSMutableSet alloc] init];
[arrServerItems addObjectsFromArray:arrServer];
NSSet *clientItems = [[NSSet alloc] init];
[clientItems addObjectsFromArray:arrClient];
[arrServerItems minus:clientItems];
This does remove the ordering information though.
For predicates you can use:
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:#"NOT (SELF IN %#)", arrClient];
depend to the predicate that you want to use:
you can use an array of arguments using this
[NSPredicate predicateWithFormat:<#(NSString *)#> argumentArray:<#(NSArray *)#>];
and build your predicate using the objects in the array
or use a predicate with block
[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
<#code#>
}]
and in the block evluate the object comparing it with the object in your array
a possible check you can do is:
return ![arrClient containsObject:evaluatedObject];
will exclude objects contained in arrClient
containsObject: use 'isEqual:' to compare the objects

iOS: FilterUsingPredicate on custom objects

I have a custom class extending NSObject. I am maintaining NSMutableArray of this class objects. Here is the situation,
customObject-class {
NSString *name;
int ID;
.....and many other properties;
}
customObjectsArray [
customObject1,
customObject2,
...etc
]
Now I am trying to use filterUsingPredicate to remove objects that has nil names, like below but it returns very few or none objects while I know that there are hundreds of objects that has name not nil or empty. Could someone please tell me what could be wrong here.
[customObjectsArray filterUsingPredicate:[NSPredicate predicateWithFormat:#"name != nil"]];
Why won't you try like this:
NSMutableArray *array=...;
[array filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
CustomObject *customObject=(CustomObject *) evaluatedObject;
return (customObject.name!=nil);
}]];
As I replied to #rdelmar, I found an issue. This predicate was getting called before customObject1's data were actually initialised. I should check the status of data flag that says data has been initialised for this particular object and then apply filter. It worked. If data is not initialised all object's name is off course nil!

remove object from NSArray

How can I remove an object from a reversed NSArray.
Currently I have a NSMutableArray, then I reverse it with
NSArray* reversedCalEvents = [[calEvents reverseObjectEnumerator] allObjects];
now I need to remove at item from reversedCalEvents or calEvents and automatically refresh the table the array is displayed in based on conditions.
i.e.
if(someInt == someOtherInt){
remove object at index 0
}
How can I do this? I cannot get it to work.
Here's a more functional approach using Key-Value Coding:
#implementation NSArray (Additions)
- (instancetype)arrayByRemovingObject:(id)object {
return [self filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"SELF != %#", object]];
}
#end
You will need a mutable array in order to remove an object. Try creating reversedCalEvents with mutableCopy.
NSMutableArray *reversedCalEvents = [[calEvents reverseObjectEnumerator] allObjects] mutableCopy];
if (someInt == someOtherInt)
{
[reversedCalEvents removeObject:object];
}
NSArray is not editable, so that you cannot modify it. You can copy that array to NSMutableArray and remove objects from it. And finally reassign the values of the NSMutableArray to your NSArray.
From here you will get a better idea...
NSArray + remove item from array
First you should read up on the NSMutableArray class itself to familiarize yourself with it.
Second, this question should show you an easy way to remove the objects from your NSMutableArray instance.
Third, you can cause the UITableView to refresh by sending it the reloadData message.
you can try this:-
NSMutableArray* reversedCalEvents = [[[calEvents reverseObjectEnumerator] allObjects] mutableCopy];
[reversedCalEvents removeLastObject];

Resources