Tracking UITableViewCells - iphone

I have a to-do list app with a bunch of tasks. Each task has a UITableViewCell. After each table view cell is tapped, it creates a view controller with the task at that row's index path's property. These view controllers are all stored in a NSDictionary. This is the code representation of what I just said:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
DetailViewController *detailVC;
if (![self.detailViewsDictionary.allKeys containsObject:indexPath]){
detailVC = [[DetailViewController alloc]initWithNibName:#"DetailViewController" bundle:nil];
[self.detailViewsDictionary setObject:detailVC forKey:indexPath];
detailVC.context = self.managedObjectContext;
}else{
detailVC = self.detailViewsDictionary[indexPath];
}
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
detailVC.testTask = task;
[[self navigationController] pushViewController:detailVC animated:YES];
NSLog(#"%#", self.detailViewsDictionary);
}
So this method of creating unique view controllers and storing them with a certain key almost always works. The problem arises when I delete or move the view controllers:
I was under the impression that a cell's gets recycled as you scroll down (dequeue). This means that marking each cell with a number identifier would result in multiple cells for the same identifier.
Also, if you stored each view controller with a indexPath key, how do you make sure the key isn't set to two view controllers..? For example. Let's say you have 4 cells, which means 4 view controllers. You delete cell 3. Cell 4 moves down to cell 3s spot. You create a new cell which goes to spot 4. Now you have two controllers with the same indexPath key! How do you avoid this?? It's screwing up my app right now because tasks that have already been moved are loading their properties in the wrong view controller/cell!
I was suggested this to solve the problem before: "You maintain an NSMutableArray that "shadows" the contents of the table." I was also suggested to use tags. However, I don't understand what how to implement these.
edit: ---random string---
In tasks.m
-(NSString *)uniqueIdentifierString{
static NSString *alphabet = #"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789";
NSMutableString *s = [NSMutableString stringWithCapacity:20];
for (NSUInteger i = 0U; i < 20; i++) {
u_int32_t r = arc4random() % [alphabet length];
unichar c = [alphabet characterAtIndex:r];
[s appendFormat:#"%C", c];
}
return s;
}
In tableviewcontroller.m
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
DetailViewController *detailVC;
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
if (![self.detailViewsDictionary.allKeys containsObject:task.uniqueIdentifierString]){
detailVC = [[DetailViewController alloc]initWithNibName:#"DetailViewController" bundle:nil];
[self.detailViewsDictionary setObject:detailVC forKey:task.uniqueIdentifierString];
detailVC.context = self.managedObjectContext;
}else{
detailVC = self.detailViewsDictionary[task.uniqueIdentifierString];
}
detailVC.testTask = task;
[[self navigationController] pushViewController:detailVC animated:YES];
NSLog(#"%#", detailVC);
NSLog(#"%#", task.uniqueIdentifierString);
}

One solution would be to decouple your VCs from the table cells. You could store the VCs in your dictionary referenced by an ID that is unique to the task, but not worry about the table cell. If the task is moved or deleted, you can handle that elsewhere without worrying about the VC array being instantly wrong.
Your other option would be to hook into the move/edit methods the logic for iterating over the VC array to adjust the array at that time.
Given that UITableView expects cells to be reused, you can see how it isn't meant to be a storage mechanism for other data.
It would be best to decouple the data so that you aren't bound to the order or existence of them.

Related

How to prevent TableViewCell from duplicating during recycle?

So my situation is pretty unique. I have a to-do list app with a bunch of tasks. Each task has a UITableViewCell. After each table view cell is tapped, it creates a view controller with the task at that row's index path's property. These view controllers are all stored in a NSDictionary. This is the code representation of what I just said:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
DetailViewController *detailVC;
if (![self.detailViewsDictionary.allKeys containsObject:indexPath]){
detailVC = [[DetailViewController alloc]initWithNibName:#"DetailViewController" bundle:nil];
[self.detailViewsDictionary setObject:detailVC forKey:indexPath];
detailVC.context = self.managedObjectContext;
}else{
detailVC = self.detailViewsDictionary[indexPath];
}
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
detailVC.testTask = task;
[[self navigationController] pushViewController:detailVC animated:YES];
NSLog(#"%#", self.detailViewsDictionary);
}
So this method of creating unique view controllers and storing them with a certain key almost always works. The problem arises when I delete or move the view controllers:
I was under the impression that a cell's index path gets recycled as you scroll down (dequeue). Doesn't that mean marking each cell with a number identifier would result in multiple cells for the same identifier?
Also, if you stored each view controller with a indexPath key, how do you make sure the key isn't set to two view controllers..? For example. Let's say you have 4 cells, which means 4 view controllers. You delete cell 3. Cell 4 moves down to cell 3s spot. You create a new cell which goes to spot 4. Now you have two controllers with the same indexPath key! How do you avoid this?? It's screwing up my app right now because tasks that have already been moved are loading their properties in the wrong view controller/cell!
I was suggested this to solve the problem before: "You maintain an NSMutableArray that "shadows" the contents of the table." However, I don't understand what this means/how to implement it.
You can use a technique we used to use on old databases. You store an NSInteger as a class var, and use that to assign a unique id to each of the cells as you create them. As you create each cell, you increment the unique id. Like this:
in your interface:
#property (nonatomic, assign) NSUInteger nextUniqueId;
then in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
cell.tag = self.nextUniqueId++;
and then track those tags in your viewControllers. Just give them an assignable property, or customize the init to include the id.
Better to set the tag value for each row in cell for row at index path method.Store that tag value globally,and use that tag in did select row method.

Adding NSMutableArray to TableView in Second Tab from AppDelegate

I have had no issues following the following tutorial: http://www.raywenderlich.com/1797/how-to-create-a-simple-iphone-app-tutorial-part-1. In essence, this tutorial teaches one to create a simple table view populated with data from a NSMutableArray and the subsequent drilling down into the DetailView of each item.
I then tried to enhance what I had by creating a TabViewController and having the created Table View as the 'Tab item 2' of this controller.
This has broken the auto-population of the Table.
The code used to populate the tables from AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
Person *person1 = [[Person alloc] initWithName:#"Monica"
balance:420
thumbImage:[UIImage imageNamed:#"01.jpg"]
fullImage:[UIImage imageNamed:#"01_t.jpg"]];
Person *person2 = [[Person alloc] initWithName:#"Ross"
balance:630
thumbImage:[UIImage imageNamed:#"02.jpg"]
fullImage:[UIImage imageNamed:#"02_t.jpg"]];
Person *person3 = [[Person alloc] initWithName:#"Rachel"
balance:420
thumbImage:[UIImage imageNamed:#"03.jpg"]
fullImage:[UIImage imageNamed:#"03_t.jpg"]];
NSMutableArray *persons = [NSMutableArray arrayWithObjects:person1, person2, person3, nil];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
UIViewController *initialViewController = (UIViewController *)[storyboard instantiateViewControllerWithIdentifier:#"RootViewController"];
self.window.rootViewController = initialViewController;
TableViewController * peopleTableViewController = [storyboard instantiateViewControllerWithIdentifier:#"PersonTableViewController"];
[peopleTableViewController setPersons: persons];
// Override point for customization after application launch.
return YES;
}
'persons' is an array of Person instances which is a property of TableViewController. The following is the population of the table based on the 'persons' array from TableViewController.m:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"PersonCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//For each person
Person *person = [self.persons objectAtIndex:indexPath.row];
cell.textLabel.text = person.name;
cell.imageView.image = person.thumbImage;
cell.detailTextLabel.text = [person getPrice];
return cell;
}
Lastly, the storyboard is located here: http://i.stack.imgur.com/9axs8.png
And just to re-iterate, this all compiles fine but during tests, the table is empty upon opening tab item #2 (where the table view controller exists as per the storyboard). This was working before I added the tabs.
I am still very new to Objective C, so please let me know if there is any other information you may require.
Your problem is that you are instantiating new view controllers rather that using the ones that are already in the storyboard. When this app starts up, the tab bar controller, as well as its content view controllers will all be instantiated. Also, because one of the controllers, the one in tab 2, is a navigation controller, its root view controller, TableViewController will also be instantiated. So, you just need to get a reference to this table view controller, not create a new one:
UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
    UINavigationController *nav = tabVC.viewControllers[1];
    TableViewController *peopleTableViewController = nav.topViewController;
    [peopleTableViewController setPersons:persons];
This should work, but I think it would be better to move the creation of the Person objects and the persons array to the viewDidLoad method of the table view controller.
I think your problem is here:
Person *person = [self.persons objectAtIndex:indexPath.row];
Try put your code in ViewDidLoad instead of AppDelegateand run it. (just for test it)

UITableView with JSON help

I am trying to feed in some JSON data to my iPhone app, the data is coming in fine as I have NSLog's telling me so.
The problem I am having is trying to get the results to show in a UITableView. I have a navigation controller underneath a tab bar controller, the navigation controller contains a table view controller which loads another NIB file with a table view connected to a class which is the delegate and data source delegate.
I also need to categorize the results into sections - these being
England
Scotland
Wales
N.Ireland
To get an idea of what JSON string I am using see this one.
As you can see the JSON does not cater for the sections but I am yet to implement this, so i would need to know beforehand so I do not have to amend much code later on.
OK - I am using Stig JSON parser.
Here is my ListVenuesView.h (connected to table view)
#import <UIKit/UIKit.h>
#import "SBJson.h"
#interface ListVenuesView : UITableViewController <UITableViewDelegate, UITableViewDataSource> {
IBOutlet UITableView *venueList;
NSMutableDictionary *jsonArray;
}
#property (nonatomic, retain) IBOutlet UITableView *venueList;
#property (nonatomic, retain) NSMutableDictionary *jsonArray;
#end
jsonArray is used to store the JSON data and eventually the proper array.
And here is my ListVenuesView.m (key areas in question)
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Table View Loaded");
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
// This is where we load the JSON data
NSURL *jsonURL = [NSURL URLWithString:#"http://www.thebigfishexperience.org.uk/sources/ajax/venue-json.php?location=peterborough"];
NSString *jsonData = [[NSString alloc] initWithContentsOfURL:jsonURL];
NSLog(#"%#", jsonData);
// Convert jsonData to array
self.jsonArray = [jsonData JSONValue];
NSLog(#"%#", jsonArray);
NSLog(#"count is: %i", [self.jsonArray count]);
// Release NSString and NSURL
[jsonURL release];
[jsonData release];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [self.jsonArray count];
}
- (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];
}
NSMutableDictionary *dict = [self.jsonArray objectAtIndex: indexPath.row];
cell.textLabel.font = [UIFont fontWithName:#"Arial" size:15.0];
cell.textLabel.text = [dict objectForKey:#"venueName"];
cell.detailTextLabel.text = [dict objectForKey:#"venueCordDist"];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
// Configure the cell...
return cell;
Also how can I use the data in the cells to go to another subview of the nav controller which gives me a back button and displays the info from the JSON string just for that particular cell that has been tapped.
I think this has something to do with it? Not sure though as this is my first app i am building! So probably expect more pleas of assistance - ha ! ;)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
/*
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:#"Nib name" bundle:nil];
// ...
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
*/
}
On selecting a row, as mentioned by u, we are navigating to another view. Let us assume that the view controller is DetailViewController which is a sub-class of UIViewController.
In the DetailViewController.h , declare a NSDictionary object.
In DetailViewController.m, add
-(void)setVenueDict:(NSDictionary*)venueDict
{
if( _venueDict )
{
[_venueDict release];
}
_venueDict = [venueDict retain];
}
In ParentViewController, ur didSelectRow.. method should be like this.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:#"Nib name" bundle:nil];
// ...
// Pass the selected object to the new view controller.
NSDictionary *dict = [self.jsonArray objectAtIndex: indexPath.row];
[detailViewController setVenueDict:dict];
detailViewController.title = [dict objectForKey:#"venueName"];
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
}
In the second view controller, u can do whatever u want with the _venueDict.
Nathan,
since you want to reuse the data parsed from the JSON feed over more than one ViewController the best way to approach this is to build an object Model so that you can pass the object for the selected row in the list to the detail ViewController.
I would also separate the JSON parsing code into a separate class and not keep it in the ViewController.
You can find classes to fetch JSON on this link.
The result from the custom code to parse the JSON feed would give back a NSDictionary with as keys the section names you mention. And the value in the NSDictionary for those keys would be an array of your custom objects that contain all the relevant data for one row (and detail screen).
Hope this helps you on your way.
jsonArray is NSMutableDictionary.
have to use
[jsonArray objectForKey:key];
//check this line
NSMutableDictionary *dict = [self.jsonArray objectAtIndex: indexPath.row];
this may help.

Calling a New View when selecting a Row in a 'UITableView'

I am currently writing my first iPhone app, but have encountered an issue. I have a view which contains a UITableView. This is the first time that I have attempted this, and this is the behaviour that I am trying to achieve:
When the user selects one of the rows, I would like this to call a new view, taking the user to a different page displaying info in reference to what they have selected.
I have it currently, so when the user selects a row it displays a UIAlert in the same view, but this doesn;t suit my needs. I have set the UITableView up through interface builder, and inputted the following code into my .m file to set it up.
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
//return the value
return 10;
}
//now we define the cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Identifier for retrieving reusable cells.
static NSString *cellIdentifier = #"MyCellIdentifier";
// Attempt to request the reusable cell.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// No cell available - create one
if(cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:cellIdentifier];
}
// Set the text of the cell to the row index.
cell.textLabel.text = [NSString stringWithFormat:#"iPad %d", indexPath.row];
return cell;
}
This creates a list of ten rows. The following codes gives me my UIAlert when tapped, however, I want to remove this and make it call a new view of my choice;
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Show an alert with the index selected.
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"iPad Selected"
message:[NSString stringWithFormat:#"iPad %d", indexPath.row]
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
Can anyone help with this last piece of code? the view I want it to call is called 'ProteinView'.
Alrighty, what we need to do is use one of the UITableView methods that are already readily available to us. We'll do the following.
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ProteinView *detailViewController = [[ProteinView alloc] initWithNibName:#"ProteinView" bundle:nil];
// It is here we'd pass information from the currently selected UITableViewCell to the ProteinView.
// An example of this is the following.
// I would do it like this, but others would differ slightly.
NSString *titleString = [[[NSString alloc] initWithFormat:#"iPad %d",indexPath.row] autorelease];
// title is an object of detailViewController (ProteinView). In my own instances, I have always made a NSString which in viewDiDLoad is made the self.navigationBar.title string. Look below for what my ProteinView.m and .h would look like.
detailViewController.stringTitle = titleString;
// ...
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
}
EDIT
// -------- ProteinView.m -------- //
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
// Here we set the navigationItem.title to the stringTitle. stringTitle is declared in the .h. Think of it as a global scope variable. It is also propertised in the .h and then synthesized in the .m of ProteinView.
self.navigationItem.title = stringTitle;
}
I haven't compiled this, so I don't know if it'll fully work. But that is definitely the fastest and most easiest way to do it!
You could present the view modally like this
YourViewController2 *viewController2 = [[YourViewController2 alloc]initWithNibName:#"YourViewController2" bundle:nil];
[self presentModalViewController:viewController2 animated:YES];
Do you have more than one view to present? If so you will need to create an array with the names, pass it into the tableview and then present the correct view for the row selected based on the indexPath.row.
you'll have to open your MainWindow.xib and add a navigation controller to it. Then add a navigation controller outlet to your app delegate and connect them. Then you'll need to set the navigation controller's view as your main window's view.
You can add a table view to any iPhone app fairly easily, just by creating a new UITableViewController subclass from the File -> New command.
Even if you go this route, I would suggest creating a new navigation-based project to use as a template/cheat-sheet.

how to display section view from pasring xml result for iphone

ok here is the code
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
#pragma mark parse DetailXml
//XMLAppDelegate *appDelegate=(XMLAppDelegate*)[UIApplication sharedApplication].delegate;
Book *aBook = [appDelegate.books objectAtIndex:indexPath.row]; i need this to get id form XMLa
**uuidd=aBook.uniqueId;** this i need
if(bdvController == nil)
// Book *aBook = [appDelegate.books objectAtIndex:indexPath.row];
bdvController = [[BookDetailViewController alloc] initWithNibName:#"BookDetailView" bundle:[NSBundle mainBundle]];
//XMLAppDelegate *appDelegate=(XMLAppDelegate*)[UIApplication sharedApplication].delegate;
//Book *aBook = [appDelegate.books objectAtIndex:indexPath.row];
DetailXml *aDetail=[appDelegate.dxml objectAtIndex:indexPath.row];**i want this as my next table secition view will be based on this XMlb but i am gettingf index out of bound
bdvController.aDetail = aDetail;**
then here xml paresing is done then this
[self.navigationController pushViewController:bdvController animated:YES];
}
i am getting index Out of bound if i do this DetailXml aDetail=[appDelegate.dxml objectAtIndex:indexPath.row];
You can't just specify the number of sections, you also have to tell the table view which cells are in which specific sections.
Depending on how you set up -tableView:cellForRowAtIndexPath:, you may only be returning table view cells for one section.
If you don't specify more than one section (via indexPath.section) when creating cells, then you will get an out-of-bounds error in -numberOfSectionsInTableView: if you return more than one section there.
You might post code for -tableView:cellForRowAtIndexPath: so we can see what's going on there. But that would be the first place I would look.