Reputation: 690
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.
@property (nonatomic, strong) NSString *searchString;
to Code.h
and synthesized it in Code.m
NSMutableSet *searchResults;
to SearchViewController.h
's @interface
performSearchWithString
and matchFound
to SearchViewController.m
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
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.
NSMutableSet
called searchResults
to SearchViewController
.NSString *
called searchString
to Code
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];
}
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
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 Code
s have three strings and your array has multiple Code
s. 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