UISearchBar Search table row with text, subtext and image - objective-c

I've noticed that in order to do a search of a table, a copy of that data must be inserted to a search array.
E.g.
//Initialize the array.
listOfItems = [[NSMutableArray alloc] init];
NSArray *countriesToLiveInArray = [NSArray arrayWithObjects:#"Iceland", #"Greenland", #"Switzerland", #"Norway", #"New Zealand", #"Greece", #"Rome", #"Ireland", nil];
NSDictionary *countriesToLiveInDict = [NSDictionary dictionaryWithObject:countriesToLiveInArray forKey:#"Countries"];
NSArray *countriesLivedInArray = [NSArray arrayWithObjects:#"India", #"U.S.A", nil];
NSDictionary *countriesLivedInDict = [NSDictionary dictionaryWithObject:countriesLivedInArray forKey:#"Countries"];
[listOfItems addObject:countriesToLiveInDict];
[listOfItems addObject:countriesLivedInDict];
//Initialize the copy array.
copyListOfItems = [[NSMutableArray alloc] init];
So what is searched is the objects that are stored in the copied array.
My Question is, how do I search Cell rows with text, subtext and image in that particular cell.

(1)
There isn't really any such thing as searching a table. What happens when the user enters text in a UISearchBar is totally up to you - you can make that operation mean anything you like. All you have to do is function as the delegate-and-data-source for the results table and form the results table in response to the standard Three Big Questions that form the basis for any table ("how many sections have you? how many rows in this section? what's the cell for this row?") in any way you like. The results table does often look like a reduced version of the original table, but this is not at all required! It can be any table you want it to be.
(2)
Don't confuse Model with View. The table is just a view. Your data is Model. It is the Model, your data that is the basis of the original table, that you are going to be searching. So when the user types in your UISearchBar and you start searching, you want to form a new Model that will be the basis of the results table. How you form it is completely up to you. Typically you'll want to filter the original model so that the only stuff left in your results model is stuff that counts as a valid result. You could do this by walking the whole original model, putting everything that matches the search criterial into the new model. Or, if the original model is an array, you could use one of the filteredArray methods to help you. The most flexible way is to form a predicate with a block, as in this example from my book:
NSPredicate* p = [NSPredicate predicateWithBlock:
^BOOL(id obj, NSDictionary *d) {
NSString* s = obj;
NSStringCompareOptions options = NSCaseInsensitiveSearch;
return ([s rangeOfString:sbc.searchBar.text
options:options].location != NSNotFound);
}];
self.filteredStates = [states filteredArrayUsingPredicate:p];
In that example, s (one item of the array) is a string each time, and I'm looking to see whether the user's search term occurs in that string. But if you had a dictionary or other structure holding both a title and a subtitle and info about an image, you could examine that dictionary in any way you like. It's just a matter of returning YES or NO according to whether this array item passes the test based on the search term, on whatever definition you attach to the notion of passing the test.
(3)
The big question remaining is when to form the results model. I usually start by making the results model identical to the original model in response to searchDisplayControllerWillBeginSearch, because otherwise the results table will say No Results while the user is typing. (That is probably why you think the first thing to do is copy the original model.) Then, I can either do the actual filtering in response to searchBarSearchButtonClicked (the user is done typing and has tapped Search), or if the model is small enough, I can filter it afresh after every letter the user types, in response to searchBar:textDidChange (the user has typed a letter in the search bar).

There are a few steps involved. Note that the code below is just an example that I'm typing in by hand now, so it probably won't compile, it's just to give you an idea.
1) Ensure that you have an array containing all the cell values.
2) Create a copy of that array, and use that copy as the data source when returning cells in your table delegate methods.
3) Set yourself up as delegate for the UISearchBar, and respond to its events:
- (void)searchBarButtonClicked(UISearchBar *)searchBar {
[self doSearch:searchBar.text];
}
- (void)searchBar(UISearchBar *)searchBar textDidChange:(NSString *)searchTerm {
if (searchTerm.length == 0) {
[self resetSearch];
[table reloadData];
}
else
[self doSearch:searchTerm];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
searchBar.text = #"";
[self resetSearch];
[table reloadData];
[searchBar resignFirstResponder];
}
4) Create the other methods
The resetSearch method just needs to copy your full data array to the data source array used by your table delegates:
- (void)resetSearch {
self.tableSourceArray = [self.dataSourceArray copy]; // Or write a deep copy if you want to.
}
Whereas when searching, we need to filter the datasource array. You may be able to create something more efficient - this is just an example.
- (void)doSearch:(NSString *)searchTerm {
NSMutableArray *filtered = [[NSMutableArray alloc] init];
for (NSString *item in self.self.dataSourceArray) {
if ([item rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location != NSNotFound])
[filtered addObject:[item copy]];
}
self.tableSourceArray = filtered;
}
And that should be it!
Tim

Related

How do I concatenate strings together in Objective-C?

So I am trying to concatenate a bunch of input strings together as one string so I can save that to a text file.
So far I am trying to write something like this
NSString *tempString = [[NSString alloc]initWithFormat:#"%#%#%#", text1, text2, text3];
The only problem with this is that I need a total of 30 strings stored this way. I need a way to do this without typing out each string name. Is there a way to use a for loop or something to accomplish this? Type the strings like this perhaps?
text(i)
So that the variable name would change each time it went through the for loop. I've tried doing something like this and I can't get it to work. If you can help me with this method or another way that you know to do it I would be very thankful.
Okay, so all of the answers here take the wrong approach (sorry guys).
The fundamental problem is that you are using your "text boxes" as a data source, when they should simply be views. When someone changes the text, you should immediately store them in your model (which could be a simple array) and then reference that model later. (This is part of MVC. Look it up if you aren't familiar, as you should be if you are programming for iOS!)
Here is what I would do. (I'm assuming that your "text boxes" are UITextField's.)
Set the delegate for each text field to your view controller.
Set the tag for each text field to a number which represents the order that you want the strings joined in. (ie 1-30)
If you don't have a separate class for your data model, then setup a declared property in your view controller which stores a reference to a NSMutableArray which can contain all of the strings in order. Let's call it dataSource. In viewDidLoad: set this to an actual mutable array filled with empty values (or previously stored values if you are saving them). The reason that we store empty values is so that we can replace them with the user entered strings for any index, even if they are entered out of order:
- (void)viewDidLoad
{
[super viewDidLoad];
self.dataSource = [[NSMutableArray alloc] initWithCapacity:20];
for(int i = 0; i < 30; i++)
[self.dataSource addObject:#""];
}
Then, use the following text field delegate method which stores the strings into the array as they are entered:
// This is called every time that a text field finishes editing.
- (void)textFieldDidEndEditing:(UITextField *)textField
{
if (textField.tag > 0)
[self.dataSource replaceObjectAtIndex:textField.tag-1 withObject:textField.text];
}
Congratulations! All of your strings are now stored in one array. Now we just have to combine them all:
NSMutableString *theString = [self.dataSource componentsJoinedByString:#""];
Note that I have NOT tested all of this so there may be typos. This should get you pointed in the right direction though!
If you set up your text boxes in Interface Builder with an IBOutletCollection(UITextField) you would have an array of text boxes that you could access the text value using KVC and join them.
//interface
...
#property (nonatomic, strong) IBOutletCollection(UITextField) NSArray *textBoxes;
//implementation
...
NSString *tempString = [[textBoxes valueForKey:#"text"]
componentsJoinedByString:#""];
Using iOS 4's IBOutletCollection
If you programmatically create your text boxes then add them to an array as you create them.
NSMutableString's appendString: method is your friend.
NSArray *strings = [NSArray arrayWithObjects: #"Hi", #" there", #" dude", nil];
NSMutableString *result = [[NSMutableString alloc] init];
for (NSString *string in strings) {
[result appendString:string];
}
NSLog(#"result: %#", result); // result: Hi there dude

Accessing the contents of an array Objective C

I have a json structure like this
I am trying to check for the "type" in each nested widgets array.So if it is of a certain type then i am trying to extract properties like fade steps,width, height etc.
Is there a way of doing it using the for loop?
Right now I am doing like this:
for (NSString *widgetArray in widgets)
{
NSLog(#"%#", widgetArray);
}
Its printing the contents, How do I extract corresponding values?
for(NSDictionary *asset in [assets allValues])
{
NSString *type = asset[#"type"];
if ([type isEqualToString:#"gradient"])
{
[gradients addObject:asset];
}
This is the pseudo code provided by one of the members which helped me access the contents of the dictionary, I am not able to apply a similar logic to the nested array structure.
Since the problem is recursive search, you have to use stack. Here are the steps:
Create NSMutableArray that will serve as the stack. Now it is empty.
Take the root widgets array and put all its objects onto the stack (-addObjectsFromArray:).
Take out the first widget (NSDictionary) in stack. Be sure to take it out of the stack (-removeObjectAtIndex:) after storing it in variable.
Do your type comparision and other stuff with that variable.
Take its subwidgets and put all of them onto the stack just like in step 2.
If the stack has something in it, repeat from step 3. Otherwise you processed all widgets.
Pseudo code:
// 1
NSMutableArray *stack = [NSMutableArray array];
// 2
[stack addObjectsFromArray:rootWidgets];
while (stack.count > 0) {
// 3
NSDictionary *widgetDict = [stack objectAtIndex:0];
[stack removeObjectAtIndex:0];
// 4
if ([[widgetDict objectForKey:#"type"] isEqualToString:...]) // Do whatever...
// 5
[stack addObjectsFromArray:[widgetDict objectForKey:#"widgets"]];
}
// 6
// You are done...
To check the type of each element, use the NSObject isKindOfClass: method.
So, you might have something like:
if ([obj isKindOfClass:[NSDictionary class]]) { ... }
Or
if ([obj isKindOfClass:[NSArray class]]) { ... }
Like #H2CO3 says, you use objectAtIndex: to access array elements and objectForKey: to access dictionary elements.
EDIT: I just realized that by "of a certain type" the OP meant the value of the "type" field, rather than the objective-c class of an entry. Still, it is often useful when reading JSON data to determine whether an entry is an array or dictionary, so I will leave my answer here.
In the latest objective C you can use subscripting like C arrays. For example,
NSLog(#"%#", widgetArray[i]);
For dictionaries you can extract via key:
NSString* id = myDict[#"id"];

How can I populate a sparse UITableView from a fetched results object?

In my sweet app you can use two kinds of items from a list of items. There are equipable items, and bag items. When you navigate to the inventory screen, there are two sections of a table view, each with 4 cells. Four cells for equipped items, and four cells for items in your "bag". If you don't have anything equipped, the cells just say "empty".
How can I populate table cells conditionally based on the shape of my results object?
Here is the code I wish I had:
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// this returns 'nil' if the index path was out of bounds
NSManagedObject * item = [self.fetchedResultsController carefullyGetObjectAtIndexPath:indexPath];
if (nil == item) {
cell.textLabel.text = #"empty";
} else {
cell.textLabel.text = [item valueForKey:#"name"];
}
}
Some problems I've encountered trying to implement this 'careful' objectAtIndexPath:
I tried bounds checking, but I'm having trouble because the fetchedArray is flat, while the conceptual model I am trying to navigate is not flat: rows are 'in' sections.
I tried using a try/catch, but then I remembered that the out-of-bounds elements are undefined, not nil... so sometimes I get an error that I can catch and react to, and sometimes I just get random interesting stuff. The setup I have now:
#try {
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
return managedObject;
}
#catch (NSException * e) {
return nil;
}
consistently gives me 5 entries, 4 in the right places and 1 odd-ball which is a repeat from the other section.
Not sure how to navigate the fetched results manually. I'm playing with four items, 2 of each kind. I had hoped it would be as easy as a doubly nested array, with sections on the outside and rows on the in. It seems that things are trickier than this. When I inspect the fetchedResults NSArray, I see that it is flat. All the "bag" items are first, and the "equipable" come next. I don't imagine I can depend on this ordering, though, so I don't feel safe making a mapping function to go from indexPath section to array index. (I could be convinced to feel safe about this).
An alternative I thought of involves populating my inventory with 8 'empty' items that would be returned as place holder results when the sections don't get completely filled. This doesn't sit well with me though.
I realize why this isn't working: tables aren't supposed to have 'empty' rows. But I want my table to have empty rows!
UPDATE
Here's the method I'm using for now...
- (NSManagedObject*) carefullyGetObjectAtIndexPath:(NSIndexPath*)indexPath
{
NSLog(#"in %# %s", [self class], _cmd);
NSString * desiredType = (indexPath.section == BAG_SECTION)? #"bag" : #"equipable";
NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];
NSInteger sectionStartIndex = -1;
// iterate until you hit a type you like
NSUInteger i, count = [fetchedObjects count];
for (i = 0; i < count; i++) {
NSObject * obj = [fetchedObjects objectAtIndex:i];
NSString * fetchedType = (NSString*)[obj valueForKey:#"type"];
if ([desiredType isEqualToString:fetchedType]) {
sectionStartIndex = i;
break;
}
}
if (-1 == sectionStartIndex) {
// maybe there aren't any of that type of item
} else {
NSInteger calculatedEntityIndex = sectionStartIndex + indexPath.row;
NSLog(#"calculated index %d", calculatedEntityIndex);
// if we are still in the array
if (calculatedEntityIndex < [fetchedObjects count]) {
// then we can get the object
NSManagedObject * entity = [fetchedObjects objectAtIndex:calculatedEntityIndex];
// and if we are still in the right section
NSString * typeForEntityAtIndex = [entity valueForKey:#"type"];
if ([desiredType isEqualToString:typeForEntityAtIndex]) {
// then this is what we wanted
return entity;
}
}
}
return nil;
}
I just really don't feel I should have to iterate over something like this...
z.
Full disclosure: this is a term project, but the project is to make the app by any means necessary. This includes third party libraries, asking questions on stackoverflow, &c... The goal of this term project is to experience working on something bigger, like a sweet app, not to learn objective-c.
Ziggy,
The NSFetchedResultsController has many limitations. If you cannot make your schema meet its exacting requirements and it appears you probably haven't, then you should construct your tableView out of multiple NSFetchRequests. It is straightforward. Most of the interesting tableViews do just this. Core Data really works best when you perform larger fetches and then refine the search using the set and filter operations on the collection classes.
tableViews can have empty rows. You just have to coordinate this with custom code in your various delegate and cell configuration code.
You have a lot of options to solve your problem. It appears you are unhappy with a quite limited framework API. You're a budding programmer. Roll your own.
Andrew
P.S. You have a curious goal to write a Mac OS X/iOS app without needing to learn Objective-C. You actually have chosen the one technology in the framework, Core Data, that exercises Objective-C to its maximum capability. (In my opinion, as an educator of iOS programmers, your education would probably be better served learning the intricacies of a very different persistence model, Core Data's model, than retreating to mundane, mainstream SQL solution.) If you would prefer to use a different language, MacRuby/RubyMotion is quite effective.

UISearchBar Issue on Table Loaded by SQLite Query

First of all, I'm new to this so excuse my any simplest question.
I'm trying to view data and filter it which is very common.
So I use different tutorials. First, I loaded data from my SQLite database, then I used custom cell tutorials and customized my cells. But I got stuck in UISearchBar.
The tutorial I followed uses an NSArray to fill the table which is declared in code-behind. Since my SQLite methods gets the data from database to an Array, I thought if I copy this array to the array in filtering methods, that would work.But it didn't.
Here is some code with explanations:
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if ([searchText length] == 0) {
[displayItems removeAllObjects];
[displayItems addObjectsFromArray:allItems];
} else {
[displayItems removeAllObjects];
for (NSString *string in allItems) {
NSRange r = [string rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (r.location != NSNotFound) {
[displayItems addObject:string];
}
}
}
[tableViewScreen reloadData];
}
Code above is for filtering and I tried to copy the array that I used to fill the table to allItems array like this:
MyProjectAppDelegate *appDelegate = (MyProjectAppDelegate *)[[UIApplication sharedApplication] delegate];
allItems = appDelegate.mySqliteArray;
Or like this:
allItems = [[NSArray alloc] initWithArray:appDelegate.mySqliteArray];
But none of them did work.
I'd like to point my problem again,I have a method that gets the data into an array in AppDelegate class, and in my TableView class, I want to copy that array to another.P.S. mySqliteArray is mutable array and allItems array is not.And also, my cells are created by custom cell class, and there are 2 labels in each row.
Since I was defining an array that has objects created by my defined class, I later realized that I was mistaken to carry objects but not strings.So I updated my code like this and succeeded to create the needed array:
for (MyClass *object in appDelegate.mySqliteArray) {
[allItems addObject:[NSString stringWithFormat:#"%#", object.objectName]];
}

Using Array Controllers to restrict the view in one popup depending on the selection in another. Not core data based

I am working on an app that is not core data based - the data feed is a series of web services.
Two arrays are created from the data feed. The first holds season data, each array object being an NSDictionary. Two of the NSDictionary entries hold the data to be displayed in the popup ('seasonName') and an id ('seasonID') that acts as a pointer (in an external table) by matches defined for that season.
The second array is also a collection of NSDictionaries. Two of the entries hold the data to be displayed in the popup ('matchDescription') and the id ('matchSeasonId') that points to the seasonId defined in the NSDictionaries in first array.
I have two NSPopUps. I want the first to display the season names and the second to display the matches defined for that season, depending on the selection in the first.
I'm new at bindings, so excuse me if I've missed something obvious.
I've tried using ArrayControllers as follows:
SeasonsArrayController:
content bound to appDelegate seasonsPopUpArrayData.
seasonsPopup:
content bound to SeasonsArrayController.arrangedObjects; content value bound to SeasonsArrayController.arrangedObjects.seasonName
I see the season names fine.
I can obviously follow a similar route to see the matches, but I then see them all, instead of restricting the list to the matches for the season highlighted.
All the tutorials I can find seem to revolve around core data and utilise the relationships defined therein. I don't have that luxury here.
Any help very gratefully received.
This is not an answer - more an extension of the previous problem.
I created MatchesArrayController and subclassed it from NSArrayController to allow some customisation.
Following the example in 'Filtering Using a Custom Array Controller' from 'Cocoa Bindings Topics', I followed the same idea as above:
MatchessArrayController: content bound to appDelegate matchesPopUpArrayData.
matchesPopup: content bound to MatchesArrayController.arrangedObjects; content value bound to MatchesArrayController.arrangedObjects.matchDescription.
I've derived the selected item from seasonPopUp:sender and used this to identify the seasonId.
The idea is to change the arrangedObjects in MatchesArrayController by defining the following in;
- (NSArray *)arrangeObjects:(NSArray *)objects
{
if (searchString == nil) {
return [super arrangeObjects:objects];
}
NSMutableArray *filteredObjects = [NSMutableArray arrayWithCapacity:[objects count]];
NSEnumerator *objectsEnumerator = [objects objectEnumerator];
id item;
while (item = [objectsEnumerator nextObject]) {
if ([[[item valueForKeyPath:#"matchSeasonId"] stringValue] rangeOfString:searchString options:NSAnchoredSearch].location != NSNotFound) {
[filteredObjects addObject:item];
}
}
return [super arrangeObjects:filteredObjects];
}
- (void)searchWithString:(NSString *)theSearchString {
[self setSearchString:theSearchString];
[self rearrangeObjects];
}
- (void)setSearchString:(NSString *)aString
{
[aString retain];
[searchString release];
searchString=aString;
}
I've used NSLog to check that things are happening the way they are supposed to and all seems ok.
However, it still doesn't do what I want.
[self rearrangeObjects]; is supposed to invoke the arrangeObjects method but doesn't. I have to call it explicity
(i.e.[matchesArrayController arrangeObjects:matchesPopUpArrayData]; )
Even then, although filteredObjects gets changed the way it is supposed to, the drop down list does not get updated the way I want it to.