3

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];
}

2 Answers 2

4

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks! I'll try that and let you know how it goes.
Well that still didn't work out the way it should. I think my if-statements are off cos they don't get skipped when the if-statement should be false. I changed isEqual: to isEqualToString: by the way.
isEqualToString is the proper method. But you are probably wrong in using OR instead of AND in your if statements. As a secondary comment, I would recommend to compare objects against nil and no NULL: the first is used with pointers to objects, the second one for non-object stuff.
Thanks for the tips! I do an OR since having a blank brand or have the string @"Show All Brands" would basically mean to skip the brand-filtering and just use the category filtering instead
That's the reason you should put AND, if you want the filter to be applied! In other words you want to filter when a brand is NOT blank (NOT nil) AND NEITHER EQUAL TO @"Show All Brands".
1

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.

1 Comment

How is this solution any different from the one I proposed in the comment on my answer? I suggested to change the ORs with ANDs, and that's exactly what you did in your own answer...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.