Solid I
Solid I

Reputation: 690

Adding a searchBar to your TableView

I'd like to add search functionality to a TableView in my app. I populate a table with an NSArray which has x amount of Objects that contain 3 NSStrings. Here's how I construct that NSArray:

First I create a class Code.h:

#import <Foundation/Foundation.h>

@interface Code : NSObject

@property (nonatomic, strong) NSString *codeName;
@property (nonatomic, strong) NSString *codeNumber;
@property (nonatomic, strong) NSString *codeDesc;

@end

Next, I synthesize these NSStrings in Code.m.

Now in my SearchViewController.m, Here's how I create my dataset:

NSMutableArray *codes;
codes = [[NSMutableArray alloc] init];

Code *c = [[Code alloc] init];
[c setCodeNumber:@"1"];
[c setCodeName:@"First Title Here"];
[c setCodeDesc:@"I might write a desc in here."];
[codes addObject:c];

c = [[Code alloc] init];
[c setCodeNumber:@"2"];
[c setCodeName:@"Second Title Here"];
[c setCodeDesc:@"2nd desc would be written here."];
[codes addObject:c];

and so on...

Here is how I display it: cellForRowAtIndexPath:

 Code *c = [codes objectAtIndex:indexPath.row];
 NSString *fused = [NSString stringWithFormat:@"%@ - %@",[c codeNumber],[c codeName]];
 cell.textLabel.text = fused;
 return cell;

So now that you know how my data is structured and displayed, do you have an idea of how to search either the NSArray or possibly (preferably) the TableCells that have already been created?

I have been through the few tutorials online regarding Adding a Search Bar to a TableView, but all of them are written for using arrays setup using simple arrayWithObjects.

SIDETHOUGHT: Is it possible for me to construct an arrayWithObjects:@"aaa-1",@"bbb-2",@"ccc-3"... from my data? If i can manage that, I can use those tutorials to populate my cells and search them!

UPDATE:

Your second answer makes plenty more sense to me! Thanks for that. I beleive I have followed your instruction, but I am getting a "-[Code search:]: unrecognized selector sent to instance 0x6a2eb20` when that line is hit.

  1. I added @property (nonatomic, strong) NSString *searchString; to Code.h and synthesized it in Code.m
  2. I added NSMutableSet *searchResults; to SearchViewController.h's @interface
  3. I added your methods performSearchWithString and matchFound to SearchViewController.m
  4. Directly under those I added this to call performSearchWithString

x

- (void)searchBar:(UISearchBar *)theSearchBar textDidChange:(NSString *)searchString {
NSLog(@"%@",searchString); //Just making sure searchString is set
[self performSearchWithString:searchString];
[self.tableView reloadData];
}

The error hits when [codes makeObjectsPerformSelector:@selector(search:) withObject:self]; runs. I am confused b/c it sounds like Code doesn't recognize searchString, but I know I added it in Code.h.

UPDATE: In order to store objects in searchResults, I had to change searchResults from a NSMutableSet to a NSMutableArray and modify - (void)matchFound:(Code *) matchingCode {} to this:

-(void) matchFound:(Code *) matchingCode {

Code *match = [[Code alloc] init];

if (searchResults.count == 0) {
    searchResults = [[NSMutableArray alloc] init];
    [match setCodeName:[matchingCode codeName]];
    [match setCodeNumber:[matchingCode codeNumber]];
    [match setCodeDesc:[matchingCode codeDesc]];
    [searchResults addObject:match];
}
else
{
    match = [[Code alloc] init];
    [match setCodeName:[matchingCode codeName]];
    [match setCodeNumber:[matchingCode codeNumber]];
    [match setCodeDesc:[matchingCode codeDesc]];
    [searchResults addObject:match];

}

With a few other tweeks, I've got a working searchbar for my tableView. Thanks Tim Kemp!

Oh, also case insensitive search was what I was looking for. NSRange rangeName = [codeName rangeOfString: searchString options:NSCaseInsensitiveSearch];

I hope this question and answer will be helpful to the next developer learning objective-c with this question!

Upvotes: 1

Views: 1416

Answers (2)

Tim
Tim

Reputation: 5054

Simpler approach

You asked for a simpler solution. This one isn't nearly as flexible, but it will achieve the same things as my earlier answer for this specific case.

Once again we are going to ask Code to search its strings for us. This time, we are going to skip the SearchRequest and the block callback and implement it directly.

In your SearchViewController you will create two methods. One to do the search, and one callback to process any results as they come back. You will also need a container to store matching Code objects (more than one might match, presumably.) You will also need to add a method to Code to tell it what the search string is.

  1. Add an ivar NSMutableSet called searchResults to SearchViewController.
  2. Add a property of type NSString * called searchString to Code
  3. Add the search method to SearchViewController. This is what you'll call when you want to initiate a search across all your codes:

    -(void) performSearchWithString:(NSString *) searchString {
       // Tell each Code what string to search for
       [codes makeObjectsPerformSelector:@selector(setSearchString:) withObject:searchString];
       // Make each code perform the search
       [codes makeObjectsPerformSelector:@selector(search:) withObject:self];
     }
    
  4. Then you will also need a callback in SearchViewController. This is so that your Code objects can tell the SearchViewController that they have found a match:

    -(void) matchFound:(Code *) matchingCode {
      [searchResults addObject:matchingCode];
      // do something with the matching code. Add it to a different table 
      // view, or filter it or whatever you need it to do.
    }
    

However do note that you don't have to use the searchResults mutable set; you may well want to just call another method to immediately add the returned result to some other list on screen. It depends on your app's needs.

In Code, add a search method a bit like we had before, but instead of the SearchRequest parameter we'll pass in a reference to the SearchViewController:

    - (void) search:(SearchViewController *) searchVC {
      // Search each string in turn
      NSRange rangeNum = [codeNumber rangeOfString : searchString];
      NSRange rangeName = [codeName rangeOfString : searchString];
      NSRange rangeDesc = [codeDesc rangeOfString: searchString];
      if (rangeNum.location != NSNotFound || rangeName.location != NSNotFound || rangeDesc.location != NSNotFound) {
         [searchVC matchFound:self];
      }
    }

Do you see how that works? If there's a match in any of the strings (|| means 'or') then pass self (which means exactly what it sounds like: the current object that's running this code right now) back to a method in the view controller called searchVC. This is called a callback because we are "calling back" to the object which originally sent us the message to do the search. We have to use callbacks rather than simple return types because we have used makeObjectsPerformSelector to tell every single Code in the codes array to do a search. We never explicitly called the search method ourselves, so we have no way to capture the return value from each search. That's why its return type is void.

You can extend matchFound to take an additional parameter which identifies which string the match was in (i.e. çodeNumber, codeName or codeDesc.) Look into enums as one good approach to pass around that kind of data.

Hope that's bit simpler.

Here is a link to an excellent language introduction/tutorial which will eliminate much confusion.

EDIT In your last comment you said that searchResults was null. I said to add it as an ivar somewhere in SearchViewController. In your initialiser method for SearchViewController you should call

searchResults = [[NSMutableSet alloc] initWithCapacity:50]` // Choose some sensible number other than 50; enough to hold the likely number of matching Code objects.

Alternatively you could 'lazy initialise' it in matchFound:

- (void) matchFound:(Code *) matchingCode {
  if (!searchResults)
    searchResults = [[NSMutableSet alloc] initWithCapacity:50];

  [searchResults addObject:matchingCode];
}

Though if you do this you should be aware that anywhere else you access searchResults may find that it's null if matchCode: has never previously been called.

Upvotes: 2

Tim
Tim

Reputation: 5054

Original, flexible and more complicated answer

I'm a little unclear as to what you're trying to do, so I'm going with your title, "Searching each string in each object of an array." In your case, your Codes have three strings and your array has multiple Codes. I assume that you need a way to tell the caller - the code that wants to do the search - which Code matches.

Here is one approach. There are easier ways but this technique is quite flexible. Broadly, we are going to make the Code object do the work of searching its own strings. We are then going to give the Code object the ability to tell the caller (i.e. the object that owns the codes array, presumably your table view controller) whether any of its strings match the search string. We will then use NSArray's method makeObjectsPerformSelector to have to tell all of its Code objects to search themselves. We will use a block for a callback.

Firstly, add a search method to Code (in the interface, or as a category depending on your design), something like this:

-(void) search:(SearchRequest *) request {
  // Search using your favourite algorithm
  // eg bool matches = [searchMe [request searchString]];
  if (matches) {
    [request foundMatch:self];
  }
}

SearchRequest is new. It's a place to tie together a search string and a callback block. It looks something like this:

@interface SearchRequest
@property (retain) NSString * searchString;
@property (copy) void (^callback)(Code *);
- (id) initWithSearchString:(NSString *) search callback:(void (^)(Code *)) callback;
- (void) foundMatch:(Code *) matchingCode;
@end

@implementation SearchRequest
// synthesize...
// initialiser sets ivars
- (void) foundMatch:(Code *) matchingCode {
  callback(matchingCode);
}

The callback block is our way of communicating back to the caller.

When you want to perform a search, construct a SeachRequest object with the string you're searching for and a block which contains the method to call when you get a match. That would look like this, in the caller:

- (void) performASearchWithString:(NSString *) searchForMe {
    SearchRequest * req = [[SearchRequest alloc] initWithSearchString:searchForMe 
                                     callback:^(Code * matchingCode) {
                                                  [self foundAHit:matchingCode];
                                     }];

    [codes makeObjectsPerformSelector:@selector(search:) withObject:req];
}

You then need to implement foundAHit in your caller, which takes the matching Code and does something with it. (You don't have to use a block: you could store a reference to the caller and a selector to call on it instead. I won't go into the arguments for either case here. Other answerers can propose alternatives.)

Upvotes: 1

Related Questions