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

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.

Related

how to implement Search feature in ios 7?

I am working on ios 7 app, I am developing animals details app. I have created first table view for animals category list, second tableview for list of animals in a particular category and third web view for animals detailed web view. I need to implement search feature in first screen that should display search items depends on user queries. How to implement this feature.
Implement search on an array through which you create your first tableview.
Take a tempArray for holding the sorted data from the initial tableView.
tempArray = [NSmutableArray alloc]init];
2.For ex lets say user enter a search string "S".
3.Loop through your array from which you created your first tableView.
for(int i = 0, i< [yourArray count], i++)
{
NSString *str = [yourArray objectAtIndex:i];
if ([str rangeOfString:#"S"].location == NSNotFound) {
NSLog(#"str does not contain your searched string");
} else {
[tempArray addObject:str];
}
}
Now Reload your tableview using your tempArray.
Just for showing the core functionality i have used a static string entered by user, you can create it dynamic using property and assigned it a string entered by user then implement the search.

Returning NO on canEditRowAtIndexPath, but can edit if scrolled

So I've been looking for an answer to this interesting issue I came across but haven't had very much luck. Basically I have a UITableView preloaded on initial app launch with a few objects using CoreData, and the ability for the user to add more.
I allow deleting of cells in the table, except for the items I have initial pre-loaded. So I perform a check in my canEditRowAtIndexPath method and return NO if the selected item is one of these pre-loaded items. Everything works great until I scroll down far enough for one of the items to be offscreen, and then when it snaps back the item that shouldn't be editable, is now editable.
I'm fairly new to iOS development, so I'm hoping this is a rather amateur issue - but I can't seem to find the answer.
Any help is appreciated.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
DataLayer *dl = [DataLayer alloc];
// Get all items from Favorites
NSArray *results = [dl FetchAll:#"Favorites"];
// Get currently selected cell properties
FavoritesTableCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
Favorites *fav = [Favorites alloc];
NSMutableArray *genItems = [[NSMutableArray alloc] init];
// Get only records that are default app items
for(int a = 0; a < [results count]; a++){
fav = [results objectAtIndex:a];
if ([fav.generic isEqualToNumber:[NSNumber numberWithInt:1]]) {
[genItems addObject:fav];
}
}
// Loop through default items to determine if editable
for (int i = 0; i < [genItems count]; i++) {
fav = [genItems objectAtIndex:i];
if ([fav.title isEqualToString:[selectedCell.nameLabel text]]) {
return NO;
}
}
return YES;
}
The root of the problem is that this method is basing it's answer on the content of a table view cell (selectedCell) rather than the model.
Table views reuse cells. As they are scrolled off the view, the "new" cells that appear are really the same object's that just disappeared on the other side of the table. So that selectedCell is not a good reference for a question that ought to be put to your model.
The code needs to be structured like this:
Your model is a NSMutableArray that starts with a few items you add. You need to know which items are originals, not to be removed:
#property (nonatomic, strong) NSMutableArray *favorites;
#property (nonatomic, assign) NSMutableArray *genericFavorites;
// at init, imagine these strings are your preloaded core data
[self.genericFavorites addObject:#"generic favorite object a"];
[self.genericFavorites addObject:#"generic favorite object b"];
[self.favorites addItemsFromArray:self.genericFavorites];
You'll use self.favorites as your model, that is when table view asks numberOfRowsInSection, you'll answer self.favorites.count. In cellForRowAtIndexPath, you'll lookup the item in self.favorites[indexPath.row] and configure the cell with data from that object. self.genericFavorites just helps you remember which objects are original, not added by the user.
If the order remains fixed, then your canEditRow logic is simple:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
return indexPath.row >= self.genericFavorites.count;
}
But if, as maybe your code implies, the user can reorder these items, then your canEditRow has more work to do, but it can do that work without reference to the table cells (which as I indicated, are unreliable):
// get the object at indexPath.row from our model. Answer NO if that object was preloaded
// from the app (if genericFavorites containsObject:), YES otherwise
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
Favorite *favorite = self.favorites[indexPath.row]; // this is your model after user has edited, reordered, etc
return ![self.genericFavorites containsObject:favorite];
}

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

UISearchBar Search table row with text, subtext and image

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

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.