Adding a searchBar to your TableView - objective-c

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.
I added #property (nonatomic, strong) NSString *searchString; to Code.h and synthesized it in Code.m
I added NSMutableSet *searchResults; to SearchViewController.h's #interface
I added your methods performSearchWithString and matchFound to SearchViewController.m
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!

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.
Add an ivar NSMutableSet called searchResults to SearchViewController.
Add a property of type 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.

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.)

Related

Using typecast for (void *) in Objective C

i am using the addToolTipRect: method to set a tooltip rect
- (NSToolTipTag)addToolTipRect:(NSRect)aRect owner:(id)anObject userData:(void *)userData
and method stringForToolTip: to obtain string value for tooltip.
- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(NSPoint)point userData:(void *)data
However the above functions work fine if i send something like
[self addToolTipRect:someRect owner:self userData:#"Tool tip string"];
But doesn't work when i send the following string. Error: BAD_ACCESS
const NSString * tooltipStr = #"Tool tip string";
[self addToolTipRect:someRect owner:self userData:tooltipStr];
In both the cases, the stringForToolTip looks like:
- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(NSPoint)point userData:(void *)data
{
id obj = (id)data;
NSString * str=nil;
if ([obj isKindOfClass:[SomeClass class]]) //This is my system defined class and works fine
{
SomeClass * someClassObj = (SomeClass *) data;
str = someClassObj.title;
}
else if([obj isKindOfClass:[NSString class]])
str = (NSString*)obj;
return str;
}
NOTE: In the stringForToolTip: method I also want to check for some other class example [obj isKindOF:[SomeClass class]] and i don't want to assert that value. The problem here is just in getting the string value by proper cast but I can't figure out how! Please tell me where I am going wrong?
edit:
What should be the right way to get the String value for tooltip in that case? should the point or tag be considered?
(void *) is not an object pointer.
That #"Tool tip string" worked was by coincidence based on the fact that is is a compile-time constant with a (essentially) permanent allocation and permanent address.
But in the code:
const NSString * tooltipStr = #"Tool tip string";
[self addToolTipRect:someRect owner:self userData:tooltipStr];
tooltipStr is an object that is kept in memory by a strong reference (retain count > 0). Since userData: does not handle objects it does not make a strong reference (does not increase the retain count) so it is released, will disappear soon becoming invalid.
Notes from the documentation:
The tooltip string is obtained from the owner. The owner must respond to one of two messages, view:stringForToolTip:point:userData: or description, use the latter. Note that NSString responds to description so you can pass an NSString for the value of owner. So, what you want is: [self addToolTipRect:someRect owner:tooltipStr userData:NULL];. There is still an issue that something must hole a strong reference to the NSString instance.
You can: [self addToolTipRect:someRect owner:#"Tool tip string" userData:NULL];
Probably the best way to go is to pass self as owner and NULL as data and implement the delegate method: view:stringForToolTip:point:userData: in the class.

NSUUID lowercase characters

I am using NSUUID for unique ids in my app like so:
[[NSUUID UUID] UUIDString]
and as is the expected result I get an id like this one: 102A21AD-7216-4517-8A79-39776B767E72
For backend reasons I need the letters in the uuid to be lowercase. I tried to call
[[NSUUID UUID] UUIDString].lowercaseString
but the returned string is empty.
Do I really have to iterate over all of the characters in the string and convert the appropriate ones to lowercase? If so does anyone have any advice of the most efficient way to do this?
EDIT:
The way I was trying to implement this was by subclassing NSUUID and then overriding the
-(NSString*) UUIDString;
method.
My implementation of this was
-(NSString*) UUIDString{
return [super UUIDString].lowercaseString;
}
The accepted answer explains why this doesn't work.
Based on your edit to the question a little investigation is in order...
It looks like NSUUID behaves like a class cluster and you cannot sub-class it without implementing it's key methods and providing the functionality of UUID generation yourself. If you do sub-class it you get a parent class whose UUIDString is the empty string. While a standard init of the class gives you back an instance of __NSConcreteUUID whose UUIDString is more useful!
If the above is confusing the following partial implementation shows one way to do this:
#interface LowerUUID : NSUUID
#end
#implementation LowerUUID
{
NSUUID *real;
}
- (id) init
{
self = [super init];
if (self)
real = NSUUID.new;
return self;
}
- (NSString *) UUIDString
{
NSString *original = [real UUIDString];
NSString *lower = original.lowercaseString;
return lower;
}
#end
To be complete you also need to provide implementations of the other methods.
For this particular class it is unlikely you'll find this worth it, but for class clusters like NSMutableArray it does make sense.
You could submit a bug report to Apple stating the documentation does not state you cannot trivially sub-class NSUUID.
This works for me:
NSString *lower = [[[NSUUID UUID] UUIDString] lowercaseString];
I tried that and it works:
NSString *str = [[NSUUID UUID] UUIDString];
NSLog(#"1: %#", str);
NSLog(#"2: %#", str.lowercaseString);

Obj-c, how do I create function which will populate an NSDictionary and gain a value from the function?

I've been reading about NSArrays and NSDictionaires and I think I need the later. I'm trying to populate an object from a small database table. So I can access the string values via a record id. I have to do this several times so putting it into an object makes sense.
I have the basics...
- (void)viewDidLoad {
// WORKING START
NSMutableDictionary *dictCategories = [[NSMutableDictionary alloc] init];
[dictCategories setValue:#"Utility" forKey:#"3"];
[dictCategories setValue:#"Cash" forKey:#"5"];
NSString *result;
result = [dictCategories objectForKey:#"3"];
NSLog(#"Result=%#", result);
// WORKING END
// Can't get this bit right, current error Request for member
// 'getCategories' in something not a structure or union
NSMutableDictionary *dictCategories2 = self.getCategories;
NSLog(#"Result2=%#", [dictCategories2 objectForKey:#"5"]);
[super viewDidLoad];
}
-(NSMutableDictionary*)getCategories {
NSMutableDictionary *dictCategories = [[NSMutableDictionary alloc] init];
[dictCategories setValue:#"Utility" forKey:#"3"];
[dictCategories setValue:#"Cash" forKey:#"5"];
return dictCategories;
}
you are calling the method wrong,try [self getCategories]
You're not being clear on what isn't working, but a few things that are obviously wrong (JonLOo might be spot on though) ...
Firstly. You're using the wrong methods, or at least there's a better one -- setValue:forKey: should/could be setObject:forKey: instead. This might be one of the reasons for your issue.
Secondly. You're over-allocating and not releasing properly. dictCategories2 in your viewDidLoad will vanish into the void and bring with it the allocated memory for dictCategories defined in the getCategories method. An easy standard fix for this is to change
NSMutableDictionary *dictCategories = [[NSMutableDictionary alloc] init];
in getCategories into
NSMutableDictionary *dictCategories = [NSMutableDictionary dictionary];
It will be autoreleased using the latter method by the system.
Thirdly. You want to read up on #property. Instead of getFoo, setBar, the Ob-C standard is to use #properties to (pre)define setters and getter methods. You can then override these to populate default data into your methods when appropriate. You also (probably) want to store the dictionary in your interface as an instance variable, rather than letting it be deallocated all the time. Example of a #property implementation that does this:
#interface foo {
NSMutableDictionary *ingredients;
}
#property (nonatomic, retain) NSMutableDictionary *ingredients;
#end
// ....
#implementation foo
#synthesize ingredients;
// ...
// the #synthesize command above will create getter and setter methods for us but
// we can override them, which we need to do here
- (NSMutableDictionary *)ingredients
{
if (ingredients != nil) {
// we've already got an ingredients variable so we just return it
return ingredients;
}
// we need to create ingredients
ingredients = [[NSMutableDictionary alloc] init];
[ingredients setObject:#"foo" forKey:#"bar"]
return ingredients;
}
In the viewDidLoad method (or anywhere else where you think ingredients might not have been initialized yet), you would do e.g.
NSMutableDictionary *dict = self.ingredients;
Anywhere else you can opt to use just ingredients without self, but if it's nil, your method will never be called, and you will get nil thrown at you.
This is useful in many cases, and is necessary if we want to ever read or write the ingredients variable from outside of our class. It's outside of what you're asking about, but I brought it up because you're trying to do something similar with self.getCategories.
Hope that helps.

NSMutableDictionary error

I want to use NSMutableDictionary to cache some data i will use later. My custom object is following:
#interface MyData : NSObject {
NSRange range;
NSMutableArray *values;
}
#property (nonatomic, retain) NSMutableArray *values;
and implement:
- (id)init {
if (self = [super init]) {
values = [[NSMutableArray alloc] init];
}
return self;
}
and when i wanna cache it, i use it like this:
NSMutableDictionary *cache = [[NSMutableDictionary alloc] init];
NSString *key = #"KEY";
MyData *data = [[MyData alloc] init];
// save some data into data
[data.values addObject:"DATA1"];
[data.values addObject:"DATA2"];
//... ...
[cache setObject:data forKey:key];
My questions is the count of cache.values is zero when i retrieve this object later as follow:
[cache objectForKey:#"KEY"];
i can retrieve "data" and the object's memory address is the same as the address when i put it into cache.
what's wrong? i need some kind guys help, any info is helpful. thanks
As Carl Norum pointed out, you're passing C strings to addObject:. addObject:, as its name suggests, requires a pointer to a Cocoa object; a C string is a pointer to characters. You need to pass NSString objects there; for literal strings, this simply requires prefixing them with #: "Fred" is a constant C string, whereas #"Fred" is a constant NSString object.
Is cache an instance variable? It looks like it's not; it appears to be a local variable, which means you're creating a new dictionary object every time. That's why there's nothing you've added previously (to previous dictionaries) in the new one. It also means you're leaking those previous dictionaries, since you're not releasing them (not in the code you showed, anyway).
Make cache an instance variable and only create the dictionary when you don't already have one (i.e., when cache == nil). Creating the dictionary in your init method is one good way. And make sure you manage its lifetime appropriately, so you don't leak and/or crash.
First of all your objects your adding don't look right it should have an # before the string. Like #"DATA1"
Second when you add an object to a dictionary or an array it does not make an actual copy of it. It just creates a pointer to it so if those objects are destroyed or moved somewhere also they are also gone out of your dictionary. A better way to make a cache of your values would be to copy the objects like so:
MyData* cache = [[MyData alloc] init];
for (int i = 0; i < [data.values count]; i ++){{
[cache.values addObject:[NSString stringWithString:[data.values objectAtIndex:i]]];
}
Don't use a dictionary in this situation.

Replace array display method?

I am curious how I might override the description method that is used when you do the following (see below) for an object. I basically want to better format the output, but am unsure about how I might go about setting this up.
NSLog(#"ARRAY: %#", myArray);
many thanks
EDIT_001
Although subclassing NSArray would have worked I instead decided that I would add a category to NSArray (having not used one before) Here is what I added ...
// ------------------------------------------------------------------- **
// CATAGORY: NSArray
// ------------------------------------------------------------------- **
#interface NSArray (displayNSArray)
-(NSString*)display;
#end
#implementation NSArray (displayNSArray)
-(NSString*)display {
id eachIndex;
NSMutableString *outString = [[[NSMutableString alloc] init] autorelease];
[outString appendString:#"("];
for(eachIndex in self) {
[outString appendString:[eachIndex description]];
[outString appendString:#" "];
}
[outString insertString:#")" atIndex:[outString length]-1];
return(outString);
}
#end
gary
If you're doing this a lot, the easiest way to reformat the display of your array would be to add a new prettyPrint category to the NSArray class.
#interface NSArray ( PrettyPrintNSArray )
- (NSSTring *)prettyPrint;
#end
#implementation NSArray ( PrettyPrintNSArray )
- (NSString *)prettyPrint {
NSMutableString *outputString = [[NSMutableString alloc] init];
for( id item in self ) {
[outputString appendString:[item description]];
}
return outputString;
}
#end
Obviously you'd need to alter the for loop to get the formatting the way you want it.
I'm assuming that you myArray variable is an instance of the NSArray/NSMutableArray class.
When NSLog() encounters the # character in its format string, it calls the -description: method on the object. This is a method on the root class, NSObject from which all other Cocoa classes inherit. -description: returns an NSString allowing any object that implements this method to be passed into NSLog(#"#",anyObject) and have a nicely formatted output. The string returned can be anything you care to construct.
For your specific problem, you could subclass NSMutableArray and override the -description: method with your own implementation. Then utilise your subclass instead of NSMutableArray.
For more information on NSObject and -description: see Apple's docs.
From Formatting string objects:
NSString supports the format characters defined for the ANSI C functionprintf(), plus ‘#’ for any object. If the object responds to the descriptionWithLocale: message, NSString sends that message to retrieve the text representation, otherwise, it sends a description message.
So to customize array conversion to string you should change NSArray descriptionWithLocale: implementation. Here's an example of how you can replace object method in run-time.