Filtering NSMutableArray with NSPredicates - ios

I've checked StackOverflow for some of the topics on NSPredicates, and although they all point in the right direction, I must be missing something essential about it all.
I've got an NSMutableArray that contains a list of products.
Each product has several properties like Brand, Category and Type.
Now I want my users to be able to filter that NSMutableArray using NSPredicates, insomuch that if any of the selected filters are blank, it shouldn't use that filter.
But, in turn, if for example all filters are on: Filter with Brand A with Category B and Type C, it should only show Brand A with Cat B and Type C.
Should I then deselect Cat B, it would filter on Brand A with Type C.
I've written some code, but it mainly returns an empty NSMutableArray, so I guess my NSPredicates are off.
I also found out that I need to default to the 'all products' NSMutableArray before running the predicate, or it will filter the already filtered array when a new filter option is selected. Should I use multiple Arrays with some BOOLean magick, or is this an issue that can be solved using NSPredicates?
Here's my code:
-(void)filterTable
{
NSPredicate *brandPredicate;
NSPredicate *categoryPredicate;
NSMutableArray *compoundPredicateArray;
if( ![self.selectedBrand isEqual: #"Show All Brands"] || !(self.currentBrand == NULL))
{
brandPredicate = [NSPredicate predicateWithFormat:#"brand CONTAINS[cd] %#",self.currentBrand];
compoundPredicateArray = [ NSMutableArray arrayWithObject: brandPredicate ];
}
if( ![self.currentCategory isEqual: #"Show All Categories"] || !(self.currentCategory == NULL))
{
categoryPredicate = [NSPredicate predicateWithFormat:#"category CONTAINS[cd] %#",self.currentCategory];
[ compoundPredicateArray addObject: categoryPredicate];
}
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:
compoundPredicateArray ];
[self.tableData filterUsingPredicate:predicate];
[self.popNTwinBee dismissPopoverAnimated:YES]; // PopoverController
[self.tableView reloadData];
}

You have a couple of conceptual errors in your code.
First, you should init your NSMutableArray of predicates as you declare it:
NSMutableArray *compoundPredicateArray = [NSMutableArray array];
Right now you only instantiate it inside your first if(), so that if the brand filter is not set the mutable array doesn't even get instantiated so adding objects to it later (for example in the second filtering if()) is uneffective and the compound predicate created empty.
Inside your first if() you will then have:
[compoundPredicateArray addObject:brandPredicate];
Your second issue is that, as you correctly imagined, you are filtering what you have already filtered previously, when you use filterUsingPredicate.
What you should be doing is to always keep the unfiltered data in a NSArray and use the filteredArrayUsingPredicate method on it to retrieve a new filtered NSArray you will use to display the data from.

Well I took a good look at the code and came up with this:
Got this handy little block of code from this site.
NSArray+filter.h
#import <Foundation/Foundation.h>
#interface NSArray (Filter)
- (NSArray*)filter:(BOOL(^)(id elt))filterBlock;
#end
NSArray+filter.m
#import "NSArray+filter.h"
#implementation NSArray(Filter)
- (NSArray*)filter:(BOOL(^)(id elt))filterBlock
{ // Create a new array
id filteredArray = [NSMutableArray array]; // Collect elements matching the block condition
for (id elt in self)
if (filterBlock(elt))
[filteredArray addObject:elt];
return filteredArray;
}
#end
And edited my method accordingly.
In TableViewController.m
- (void)filterTable {
WebServiceStore *wss = [WebServiceStore sharedWebServiceStore];
self.allProducts = [wss.allProductArray mutableCopy];
NSArray *filteredOnBrand;
NSArray *filteredOnCategory;
NSArray *filteredOnCategoryAndBrand;
if (![self.currentBrand isEqualToString:#"All Brands"] && !(self.currentBrand == nil))
{
filteredOnBrand = [self.allProducts filter:^(id elt)
{
return [[elt brand] isEqualToString:self.currentBrand];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnBrand];
}
if ([self.currentBrand isEqualToString:#"All Brands"] || self.currentBrand == nil)
{
filteredOnBrand = [self.allProducts mutableCopy];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnBrand];
}
if (![self.currentCategory isEqualToString:#"All Categories"] && !(self.currentCategory == nil))
{
filteredOnCategory = [self.allProducts filter:^(id elt)
{
return [[elt category] isEqualToString:self.currentCategory];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnCategory];
}
if (![self.currentCategory isEqualToString:#"All Categories"] && !(self.currentCategory == nil) && ![self.currentBrand isEqualToString:#"All Brands"] && !(self.currentBrand == nil)) {
filteredOnBrand = [self.allProducts filter:^(id elt) {
return [[elt brand] isEqualToString:self.currentBrand];
}];
filteredOnCategoryAndBrand = [filteredOnBrand filter:^(id elt) {
return [[elt category] isEqualToString:self.currentCategory];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnCategoryAndBrand];
}
}
You should also reload the table data afterwards of course, but I used a custom method for that, which I left out.

Related

Custom NSSortDescriptor for NSFetchedResultsController

I have a custom "sort" that I currently perform on a set of data. I now need to perform this sort on the fetchedObjects in the NSFetchedResultsController. All of our table data works hand in hand with core data fetched results so replacing the data source with a generic array has been problematic.
Since NSFetchedResultsController can take NSSortDescriptors it seems like that is the best route. The problem is I don't know how to convert this sort algorithm into an custom comparator.
How do I convert this into a custom comparator (if possible)? (if not how do I get the desired sorted result while using NSFetchedResultsController). In essence the field 'priority' can be either 'high' 'normal' or 'low' and the list needs to be sorted in that order.
+(NSArray*)sortActionItemsByPriority:(NSArray*)listOfActionitemsToSort
{
NSMutableArray *sortedArray = [[NSMutableArray alloc]initWithCapacity:listOfActionitemsToSort.count];
NSMutableArray *highArray = [[NSMutableArray alloc]init];
NSMutableArray *normalArray = [[NSMutableArray alloc]init];
NSMutableArray *lowArray = [[NSMutableArray alloc]init];
for (int x = 0; x < listOfActionitemsToSort.count; x++)
{
ActionItem *item = [listOfActionitemsToSort objectAtIndex:x];
if ([item.priority caseInsensitiveCompare:#"high"] == NSOrderedSame)
[highArray addObject:item];
else if ([item.priority caseInsensitiveCompare:#"normal"] == NSOrderedSame)
[normalArray addObject:item];
else
[lowArray addObject:item];
}
[sortedArray addObjectsFromArray:highArray];
[sortedArray addObjectsFromArray:normalArray];
[sortedArray addObjectsFromArray:lowArray];
return sortedArray;
}
UPDATE
Tried using a NSComparisonResult block but NSFetchedResultsController does not allow that
Also tried using a transient core data attribute and then calculating a field that I could sort. But the sort takes place before the field is calculated so that didn't work.
I tried setting sections for each priority type - which worked. But it wasn't displaying in the right order and apparently you cannot order sections with core data.
Any other thoughts?
How about this?
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:#"priority" ascending:YES comparator:^NSComparisonResult(id obj1, id obj2) {
if ([obj1 isEqualToString:obj2]) {
return NSOrderedSame;
} else if ([obj1 isEqualToString:#"high"] || [obj2 isEqualToString:#"low"]) {
return NSOrderedAscending;
} else if ([obj2 isEqualToString:#"high"] || [obj1 isEqualToString:#"low"]) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];

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

Best way to find object in Array

I have array with arrays ex.:
(
(
object,
object
),
(
object,
object,
object,
object
)
)
Each objecthas property .objectID.What is the best way to find object with specific objectID?
Here are two options for you:
Option 1: using nested for loops
CustomObject *searchingObject;
// searching through the first array (which has arrays inside of it)
// Note: this will stop looping if it searched through all the objects or if it found the object it was looking for
for (int i = 0; i < [firstArray count] && searchingObject; i++) {
// accessing the custom objects inside the nested arrays
for (CustomObject *co in firstArray[i]) {
if ([co.objectId == 9235) {
// you found your object
searchingObject = co; // or do whatever you wanted to do.
// kill the inside for-loop the outside one will be killed when it evaluates your 'searchingObject'
break;
}
}
}
Option 2: using blocks:
// you need __block to write to this object inside the block
__block CustomObject *searchingObject;
// enumerating through the first array (containing arrays)
[firstArray enumerateObjectsUsingBlock:^(NSArray *nestedArray, NSUInteger indx, BOOL *firstStop) {
// enumerating through the nested array
[nestedArray enumerateObjectsUsingBlock:^(CustomObject *co, NSUInteger nestedIndx, BOOL *secondStop) {
if ([co.objectId == 28935) {
searchingObject = co; // or do whatever you wanted to do.
// you found your object now kill both the blocks
*firstStop = *secondStop = YES;
}
}];
}];
Although still considered N^2 execution time these will only run as far as they need to. Once they find the object they cease searching.
try it with
[ary filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"objectID == %#", objectID]];
--
id object = nil;
NSPredicate *pred = [NSPredicate predicateWithFormat:#"objectID == %#", objectID];
for(NSArray *subAry in ary)
{
NSArray *result = [subAry filteredArrayUsingPredicate:pred];
if(result && result.count > 0)
{
object = [result objectAtIndex:0];
break;
}
}
for Zombies cares everyone himself :P
If you aren't concerned with order, you could instead use an array of dictionaries where the objectId is the key. That makes your search O(N).

Understanding how to use UISearchBar with Core Data

I have an iPad app (Xcode 4.6, iOS 6.2, ARC and Storyboards). I have a UITableView that contains prototype cells, with two labels (lName and lPhone). I have filled a NSArray with the results of the Core Data store. I copied the code from a sample, and am lost! I have two fields I am looking for: name and phone number. I want to be able to search on either one. I tried using the UISearchBar Controller, but the results span the entire window, which is not acceptable. So, I'm trying to do this without the controller. I want the search to filter the shown entries in the UITableView, which this bit of code is supposed to do.
When I do the MR_findAll (MagicalRecord), I get all of the attributes in the Core Data store. This is where I'm lost - how do I get the two attributes out of the array and into the NSMutableArray allTableData, or is it even necessary in this case?
This is my code, so far:
NSArray *allDataArray = [ClientInfo MR_findAll];
// move objects from Core Data store to NSMutablearray
[allTableData addObjectsFromArray:allDataArray];
if(text.length == 0) {
isFiltered = FALSE;
}
else {
isFiltered = true;
filteredTableData = [[NSMutableArray alloc] init];
for (ClientCell* client in allTableData) {
NSRange nameRange = [client.lName.text rangeOfString:text options:NSCaseInsensitiveSearch];
NSRange phoneRange = [client.lPhone.text rangeOfString:text options:NSCaseInsensitiveSearch];
if(nameRange.location != NSNotFound || phoneRange.location != NSNotFound) {
[filteredTableData addObject:client];
}
}
}
I also don't understand how the NSRange is going to match against the two fields I'm looking for. I'm really confused here.
The rangeOfString method returns an NSRange with a location that's not equal to NSNotFound when a given substring is found in the receiver string. What your code does is that it first checks the range of the search text in client.lName.text and client.lPhone.text. Next, the code adds the object to filteredTableData if either of the ranges exist.
As for adding only your two attributes to the filteredTableData, this is simply not needed, as you should access the already stored object to fetch these attributes.
Finally, I'd also recommend you have a look at the free Sensible TableView framework as it should help you perform these kind of searches automatically.
You have to Have to do some thing like this
Fetch the Data from coredata into an array which is pretty mandatory and you have lot of tutorials on that.
And then in the search bar delegate method do implement some thing like this. Which will start filtering your array so that you can see your desired results
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
self.tableView.allowsSelection = YES;
self.tableView.scrollEnabled = YES;
NSArray *list = [[NSArray alloc] initWithArray:artists];
if (searchBar.text && [searchBar.text length] > 0)
{
NSMutableArray *filterContacts = [[NSMutableArray alloc]initWithArray:list];
// NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title LIKE %#",searchBar.text ];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"title CONTAINS[c] %#",searchBar.text ];
[filterContacts filterUsingPredicate:predicate];
artists = filterContacts;
}
[self.tableView reloadData];
[self updateSearchString:searchBar.text];
}

Search NSArray of NSDictionary (which contains NSArray of NSDictionary, repeatedly)

I have a data-structure (in plist) that looks something like this:
What i have here is an NSArray of NSDictionary. Each NSDictionary has two keys:
Title
Link (recursive)
This forms a tree like structure, with variable length branches i.e. some branches can die at level 0, and some can be as large as level 3 or more.
I'm showing this structure in UITableView (with a little help from UINavigationController). This was easy enough.
Note: On tapping the Leaf Node
(represented by NSDictionary object
with Nil or Zero as "Link"), an
event is triggered i.e. Model window
appears with some information.
Now, i need to add Search support.
Search bar will appear above UITabeView (for Level 0). I need to come-up with a way to search this tree like structure, and then show the results using UISearchDisplayController, and then allow users to navigate the results as well.
How?... is where i'm a little stuck
and need some advise.
The search has to be quick, because we want search as you type.
p.s. I've thought of translating this data structure to CoreData, and it's still lurking in my mind. If you think it can help in this case, please advise.
Edit:
Here's my current solution, which is working (by the way):
#pragma mark -
#pragma mark UISearchDisplayController methods
- (void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar {
NSLog(#"%s", __FUNCTION__);
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
NSLog(#"%s", __FUNCTION__);
[self filterCategoriesForSearchText:searchString
scope:[controller.searchBar selectedScopeButtonIndex]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption {
NSLog(#"%s", __FUNCTION__);
[self filterCategoriesForSearchText:[controller.searchBar text]
scope:[controller.searchBar selectedScopeButtonIndex]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
#pragma mark UISearchDisplayController helper methods
- (void)filterCategoriesForSearchText:(NSString *)searchText scope:(NSInteger)scope {
self.filteredCategories = [self filterCategoriesInArray:_categories forSearchText:searchText];
NSSortDescriptor *descriptor = [[[NSSortDescriptor alloc] initWithKey:KEY_DICTIONARY_TITLE ascending:YES] autorelease];
[self.filteredCategories sortUsingDescriptors:[NSArray arrayWithObjects:descriptor, nil]];
}
- (NSMutableArray *)filterCategoriesInArray:(NSArray *)array forSearchText:(NSString *)searchText {
NSMutableArray *resultArray = [NSMutableArray array];
NSArray *filteredResults = nil;
// Apply filter to array
// For some weird reason this is not working. Any guesses? [NSPredicate predicateWithFormat:#"%# CONTAINS[cd] %#", KEY_DICTIONARY_TITLE, searchText];
NSPredicate *filter = [NSPredicate predicateWithFormat:#"Title CONTAINS[cd] %#", searchText];
filteredResults = [array filteredArrayUsingPredicate:filter];
// Store the filtered results (1)
if ((filteredResults != nil) && ([filteredResults count] > 0)) {
[resultArray addObjectsFromArray:filteredResults];
}
// Loop on related records to find the matching results
for (NSDictionary *dictionayObject in array) {
NSArray *innerCategories = [dictionayObject objectForKey:KEY_DICTIONARY_LINK];
if ((innerCategories != nil) && ([innerCategories count] > 0)) {
filteredResults = [self filterCategoriesInArray:innerCategories forSearchText:searchText];
// Store the filtered results (2)
if ((filteredResults != nil) && ([filteredResults count] > 0)) {
[resultArray addObjectsFromArray:filteredResults];
}
}
}
return resultArray;
}
Core Data would be able to perform the search in the data store pretty efficiently, and would scale the search to more levels efficiently. Also, if you use NSFetchedResultsController for the TableView it would almost certainly be more memory efficient - the worst case would only have one level array loaded at any given time. And the best case is considerably better, as it would only have faulted a few objects into the array. HTH

Resources