Understanding how to use UISearchBar with Core Data - ios

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

Related

iOS - searching in a large NSArray is slow

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.

UISearchBars. Copy Array always equals to Nil

- (void)searchTableList {
NSString *searchString = searchBar.text;
NSString *str=[[stories valueForKeyPath:#"name"] componentsJoinedByString:#"#"];
NSLog(#"desired string:%#",str);
NSMutableArray *array = [[NSMutableArray alloc]init];
array = [str componentsSeparatedByString:#"#"];
//Never attempt to use compare with an array of dictionaries have to extract strings first first
for (NSString *tempStr in stories) {
NSComparisonResult result = [tempStr compare:searchString options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) range:NSMakeRange(0, [searchString length])];
if (result == NSOrderedSame) {
[filteredContentList addObject:tempStr];
}
}
}
First of all, filteredContentList is never allocated in your code above, so it will always point to nil. Add smth like filteredContentList = [NSMutableArray array] in viewDidLoad.
Secondary, you rely on isSearching boolean flag to detect wether your are dealing with search results table view or general table view of you controller. This is, IMHO, bad practice.
You should rely on tableView parameter, which is being passed to every method of table views delegates (in your case -- your UITableViewController). Set tags or compare tableView parameter to self.tableView.
Last thing -- you do not need to call reloadData at viewDidLoad.

NSDictionary Searching Issue

I have a plist (an array of dictionary's) that I am reading into an NSArray which I am using to populate a table. It's a list of people and their work location, phone number, etc. I added a UISearchBar and implemented the textDidChange method as well.
When I search by the person's last name I do see the filtered list in the table, however I don't think that I am storing the filtered results properly. I am using an NSMutable Array but I am losing the key:value pairing.
Can someone please point me in the right direction? I ultimately would like to click on a filtered name and push to a detailed view controller. I believe my issue is that I am trying to capture the filtered results in an NSMutableArray but I am not certain.
I've done a lot of Googling but can't seem to put this together in my head. Any help is appreciated.
Kind Regards,
Darin
Here is the array that I am using to load the plist.
-(NSArray *)content
{
if (!content){ {
content = [[NSArray alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ophonebook" ofType:#"plist"]];
NSSortDescriptor* nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"Last" ascending:YES];
content= [content sortedArrayUsingDescriptors:[NSArray arrayWithObject:nameSortDescriptor]];
}
return content;
}
Here is the UISearchBar Method
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (searchText.length == 0)
{
isFiltered= NO;
} else {
isFiltered= YES;
filteredPeople = [[NSMutableArray alloc]init];
for (NSDictionary *dict in content)
{
NSString *last = [dict objectForKey:#"Last"];
NSRange lastRange = [last rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (lastRange.location != NSNotFound)
{
[filteredPeople addObject: [dict objectForKey:#"Last"]];
}
}
}
[myTableView reloadData];
}
Keep things simple, get rid of the flags and the different arrays in your table delegate methods. Instead, have 1 array for your source data (content) and another array for your source data that is actually for display (call it displayContent).
Now, when you start up, set displayContent = content.
Then, when you want to filter, set displayContent = [content filteredArrayUsingPredicate:...]; (you can convert your current loop into a simple predicate).
Finally, when you're done searching, set displayContent = content.
No flags. No ifs in the table delegate methods. Simple, readable code.
p.s. your problem is:
[filteredPeople addObject: [dict objectForKey:#"Last"]];
which you should be setting to:
[filteredPeople addObject:dict];
so you have all the data instead of just the names. Though technically you could still make it work by searching for the last name in your content.

Filtering NSMutableArray with NSPredicates

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.

Get distinct entity objects from NSMutableArray using NSPredicate in iPhone sdk [duplicate]

This question already has answers here:
Removing duplicates from NSMutableArray
(9 answers)
Closed 10 years ago.
I have an NSMutableArray which has entity class object as its objects.
Now I want to remove the distinct objects from it. Consider following example
Entity *entity = [[Entity alloc] init];
entity.iUserId = 1;
entity.iUserName = #"Giri"
[arr addObject:entity];
Entity *entity = [[Entity alloc] init];
entity.iUserId = 2;
entity.iUserName = #"Sunil"
[arr addObject:entity];
Entity *entity = [[Entity alloc] init];
entity.iUserId = 3;
entity.iUserName = #"Giri"
[arr addObject:entity];
Now I want only two objects in the Array by removing the duplicate iUserName. I know the way by iteration but I want it without iterating it like predicate or some other way.
If anyone knows then please help me.
I had tried using [arr valueForKeyPath:#"distinctUnionOfObjects.iUsername"]; but it does not return me the entired object.
This question is totally different than the questions which are asked previously. Previously asked question is for getting the distinct objects is correct but they uses looping & I don't want this. I want it from NSPredicate or any other simple option which avoids looping.
EDIT: You can't do what you want to without looping over the array manually and building up a new array. The answer below won't work because it assumes that there are only at most two duplicates.
NSMutableArray *filteredArray = [NSMutableArray array];
for (Entity *entity in arr)
{
BOOL hasDuplicate = [[filteredArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"iUserName == %#", entity.iUserName]] count] > 0;
if (!hasDuplicate)
{
[filteredArray addObject:entity];
}
}
This will look for duplicates in the filtered array as it builds it.
Begin Original Answer
You can't use an NSSet because the Entity instances would have to return NSOrderedSame in compareTo:, which isn't a good idea since you shouldn't use names as unique identifiers.
You can use predicates, but they'll still loop over the array in an O(n^2) time without some optimization.
NSArray *filteredArray = [arr filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(Entity *evaluatedObject, NSDictionary *bindings) {
return [[arr filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"iUserName == %#", evaluatedObject.iUserName]] count] > 1;
}]];
That'll work fine. You could make it even faster by sorting the array by the iUserName property first and doing a linear scan over the sorted array (stopping when you see the first duplicate). That's a lot of work if you're dealing with small sample sizes (say, under ten thousand or so). It's probably not worth your time, so just use the code above.
Well you have a few options (that I can think of).
Use a NSSet instead of a NSArray.
Use a for loop (but you don't want to iterate through the array)
Use a predicate search iUserName to see if the name exists before adding it to the array.
Something like:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"iUserName == 'Giri'"];
NSArray *searchArray = [arr filterUsingPredicate:predicate];

Resources