What's the best way to return different cells depending on position in a UITableView - iphone

I have a grouped UITableView that has 3 sections and a number of cells in each section that each need to be a different custom cell, with different display requirements.
I currently have a large if statement in cellForRowAtIndexPath, with each branch instantiating and returning the appropriate custom cell based on interrogating the indexPath's section and row properties. e.g.
if (indexPath.section == 0 && indexPath.row == 0) {
// instantiate and return cell A
} else if (indexPath.section == 1 && indexPath.row == 2) {
// instantiate and return cell B
} //etc etc
What is best practice for this scenario? A large if statement does the job, but is it the best implementation?

By far the best way to do this is described in this post from Cocoa With Love. Basically it's a custom controller for each cell type but it ends up being really simple to implement.

One method I've used once or twice is a nested series of NSArray objects that I initialize in viewDidLoad:
// sec<n>Row<n>Cell are IB Outlets to UITableViewCell objects defined in your .xib
NSArray *firstSectionRows = [[NSArray alloc] initWithObjects: sec1row1Cell, sec1row2cell, nil];
NSArray *secondSectionRows = [[NSArray alloc] initWithObjects: sec2row1Cell, sec2row2cell, nil];
// cellTree is an instance variable
cellTree = [[NSArray alloc] initWithObjects: firstSectionRows, secondSectionRows, nil];
[firstSectionRows release];
[secondSectionRows release];
Then in your tableView:cellForRowAtIndexPath: method would look like this:
...
return [[cellTree objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
...
This assumes that each of your cells is unique, in which case you don't even need to try and dequeue reusable cells. If you have more than one of the same type of cell in any section, you'll have to create/dequeue them appropriately and assign them a unique cell-type identifier.
If you have a large or complicated structure you might be able to do your array setup in a .plist file and use NSArray's initWithContentsOfFile: method to read it in. However you will have to do some kind of KVC magic to get cell objects from the strings in your array:
return [self valueForKey:[[cellTree objectAtIndex:indexPath.section] objectAtIndex:indexPath.row]];

On complex table views I've considered splitting up the UITableViewDataSource routines into sub-delegates, one delegate for each section. So for 3 sections, I would create 3 separate objects each one responsible for providing the UITableViewDataSource methods for that section of the table. Then the UITableView's delegate just dispatches to one of the appropriate sub-delegates depending on which section the request is for.

I think this is pretty much common practice. Just be sure that you specify a unique cell identifier for those different cells. So that caching of the cells works properly.

Related

UTableView DataSource Setup

Just wondering why we can't we set the data source to UITableview in the below way rather than returning the data count in one method and returning the data for cell in different method.
Why don't we do something like below?.
UITableView *tableView = [UITableView alloc] init];
tableView.datasource = [NSArray arrayWithObjects:#"row1", #"row2", nil];
tableView.cellIdentifier = #"Identifier";
tableView.cell = [UITableViewCell alloc] init];
//delegate methods implemented as properties,
tableView.rowHeight = 50.0;
tableView.headerHeight = 100.0
tableView.headerView = [UIView alloc] init];
[self.view addSubView:tableView];
We can do this right, may not be 100 % correct, we can think of adding some more properties to make the TableView in the same way it works now.
Whey Apple designed the TableView in the way it works now.Helpful if some one could explain the pros and cons of this design with the existing design.
The problem with this implementation is that you are not getting dequeued cell which will spike your's device memory once you reached up to app's allocated maximum level of memory. On every table row, a new cell is created.
Moreover cellForRowIndexPath method gives you flexibility in customising cell based on row index path.
What if you are rendering two different custom cell, which contains different views, how can you differentiate between them without using cellForRowIndex delegate's method.

UITableViewCell Accessory Repeating?

In my app, I have a detailed view where the user can edit attributes of for example a person, their name, address etc.
In two cells, rather than being able to select them to edit their contents, they have a right accessory, a UISwitch, however sometimes, its inconsistent, but they replicate onto other cells in my last section.
I have been scanning my code dozens of times over with a fine comb and can't find the damn cause. What might cause this? Here is the code that I use to create the UISwitch on just a single cell:
if (indexPath.section == 0 && indexPath.row == 1)
{
cell.textLabel.text = #"Confirmed";
//Make the cell unselectable
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
//Create and add uiswitch
confirmedSwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
[confirmedSwitch addTarget:self action:#selector(switchConfirmedStatus:) forControlEvents:UIControlEventValueChanged];
[confirmedSwitch setOn:[venue.isConfirmed boolValue]];
cell.accessoryView = confirmedSwitch;
}
So you expect it to only show up on that cell, see anything odd with that code? I have checked my if statements and all my brackets indexPath checks are correct.
Anyone see this before or have any clues?
The problem is because of reusability issues in the UITableView. You probably use the same identifier for all cells and this causes the cellForRowAtIndexPath to be implemented in other rows (when you scroll up and down).
This is a common problem when you are developing applications with tableView and there are plenty of questions like this on StackOverflow.
As a general solution, you will need to do either of these.
Use different identifiers when you assign dequeueReusableCellWithIdentifier for each cell. This is fairly simple, and you just need to assign different identifiers for them.
Subclass you UITableViewController, and create your own CustomTableViewController which will implement the necessary components in the cell. I believe you will need to override the set Layout Subviews method.
Take a array in view will appear and add object 0 for this row and 1 for all other rows and then in cellForRowAtIndexPath check if array has 0 at that index if it has then put switch otherwise nill..
for(int i =0;i<10;i++)
{
if(i==1)
{
[arrayForSwitch addObject:#"0"];
}
else
{
[arrayForSwitch addObject:#"1"];
}
}
then in cellForRowAtIndexPath write condition
if([arrayForSwitch objectAtIndex:indexPath.row isEqualToString:#"0"])
{
cell.accessoryView = confirmedSwitch;
}
else
{
cell.accessoryView =UITableViewCellAccessoryNone;
}
it will now remain same

How to call/pass an array value from a table cell into a view?

As novice/beginner programmer for iOS, I unfortunately have come across a roadblock that I would like some assistance with. Using storyboards, I have created a table view that has a single prototype cell. This cell then gets populated with information that has been passed from an array (two labels, titleLabel and timeLabel, as well as a "type" of cell that is represented by an integer, typeOfCell), allowing multiple cells to be generated by the array, while only using the single prototype cell "template". What I am trying to accomplish is to allow the user to select a cell from the table, and then have a view come up that displays the information in that cell. Since I am a beginner, please give explicit instructions should you choose to answer this question. Any feedback is greatly appreciated, thank you for your time and answers.
So if I get this correctly, you click on a cell in the table, you want to move to different view? That is what is the UINavigationController is supposed to do. Here is a link UINavigationController apple documentation link.
Hope this is what your looking for.
If you are looking at passing the values from the tablecell to the view that is called, then you can create member variables in the view corresponding to the values to be passed, synthesize it and pass the values in the didSelectRow method.
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
CustomTableCell *lCell = (CustomTableCell*)[tableView cellForRowAtIndexPath:indexPath];
if(indexPath.row == x) //x being the row required
{
NewViewController *lNewVC = [[NewViewController alloc] init]; //This class will have titleLabel, timeLabel and cellType as member vars
lNewVC.titleLabel = cell.titleLabel;
lNewVC.timeLabel = cell.timeLabel;
lNewVC.cellType = cell.typeOfCell;
[self.navigationController pushViewController:lNewVC animated:YES];
}
else
/* similar code for other rows */
}

How can a UITableViewCell know of its own indexPath?

The standard Grouped UITableView style allows UITableViewCells to be drawn with rounded corners at the top and bottom of each section. How is this accomplished? How does the cell know its own location within its section, and how does it know when to change its rounded edges?
I want to make my own rounded cells, and I have images to use, but don't know when to show which image
Note: I already know how the UITableView works, and I know how to use it. I just thought that since a UITableView is able to automatically draw rounded corners at the correct places, I should be able to as well, without needing to add anything to my data source or delegate.
NSIndexPath *indexPath = [(UITableView *)self.superview indexPathForCell: self];
int rows = [(UITableView *)self.superview numberOfRowsInSection:indexPath.section];
if (indexPath.row == 0 && rows == 1) {
// the one and only cell in the section
}
else if (indexPath.row == 0) {
//top
}
else if (indexPath.row != rows - 1) {
//middle
}
else {
//bottom
}
It's very simple. suppose cell is the object, whose position is to be found out.
UITableView* table = (UITableView *)[cell superview];
NSIndexPath* pathOfTheCell = [table indexPathForCell:cell];
NSInteger sectionOfTheCell = [pathOfTheCell section];
NSInteger rowOfTheCell = [pathOfTheCell row];
There is sectionLocation method of UITableViewCell that returns integer telling you what you need:
1 - middle cell
2 - top cell
3 - bottom cell
4 - single cell
I had no issues using this in several production apps since 2010.
UPDATE: one of our binaries was automatically rejected recently (end of 2018) because we were using 'sectionLocation' property, so it's not a good option anymore.
Add something like this into your header files and you can use it:
typedef NS_ENUM(NSInteger, MMMTableViewCellLocation) {
MMMTableViewCellLocationUndefined = 0,
MMMTableViewCellLocationMiddle = 1,
MMMTableViewCellLocationTop = 2,
MMMTableViewCellLocationBottom = 3,
MMMTableViewCellLocationSingle = 4
};
#interface UITableViewCell ()
/** Undocumented method of UITableViewCell which allows to know where within section the cell is located,
* so the cell can draw its borders properly. */
- (MMMTableViewCellLocation)sectionLocation;
/** Override this one to know when the value of sectionLocation changes. */
- (void)setSectionLocation:(MMMTableViewCellLocation)sectionLocation animated:(BOOL)animated;
#end
You can use
- (NSIndexPath *)indexPathForCell:(UITableViewCell *)cell
for this issue. In my example I am using this to scroll the cell (with custom content) to the top of the view.
If you need more robust and general stuff, take a look at http://cocoawithlove.com/2009/04/easy-custom-uitableview-drawing.html - Matt Gallagher shows what you need, pretty effectively. He basically recreates UITableViewController from UIViewController, while adding ability to use your own custom graphics. I'm just working on applying this to one my projects, so far it looks it would do the job.
Unfortunately, I have found no solution to this problem, and have resorted to subclassing UITableViewController and UITableViewCell into a generic solution that I can extend as necessary.
You don't do this in cell. Rounded corners are drawn in [tableView viewForHeaderInSection] and viewForFooterInSection.
The way I do it is to use Plain tableview style, then use these two views for roundness and cells are normal, no rounds.
Without getting into who draws what, you can know which cell is the last cell in its section inside of cellForRowAtIndexPath very easily.
You're passed in the indexPath of the cell you need to provide, right? You're also passed the tableView.
call [tableView numberofRowsInSection:indexPath.section] and if it's == ([indexPath.row]-1) you know you're being asked to supply the last cell in that section.
At the time that cellForRowAtIndexPath is being called, the cell is guaranteed to be at the indexPath passed in.
To expand upon Darren's answer (which I found most useful, thanks Darren!), what you can do is to iterate through all of the superviews' until you find the parent UITableView. This should be future proof since you do not rely on a fixed hierarchy of views.
I use a recursive method that will return the UITableView if it finds one or return nil if there is none.
- (UITableView *)parentTableViewOf:(UIView *)view {
Class class = [view.superview class];
NSLog(#"Class : %#", NSStringFromClass(class));
if([view.superview isKindOfClass:[UITableView class]]) {
return (UITableView *)view.superview;
} else {
return [self parentTableViewOf:view.superview];
}
return nil;
}
So far I've used this one and it seems to work without hiccups. Hope it helps! :)
The cells dont know where they go...The table view has cells, You are the one telling the table view WHAT goes in the cell. You do this in the DataSource where you implement cellForRowAtIndexPath...The way this works :
An index path has a row and a section
For a grouped table view
A section pertains to a group, and a row pertains to 1 entry in that section,
the way UITableView knows how many rows are in a section and how many sections there are is the DataSources methods numberOfSectionInTableView and the method numberOfRowsInSection, this will make the right calls to cellForRowAtIndexPath, it is up to you to recognize which section and row is being queried and you need to build your cell according to these specifications.
A good way to do this i s you can have a Dictionary with keys of section names and values of NSArray with the values that go in that section.
So you implementation for numberOfSectionsInRows would look like
return [[dictionary allKeys] count]
And the implmentation of numberOfRowsInSection would look like
NSString* key=[[dictionary allKeys] objectAtIndex:sectionNumber]
return [[dictionary objectForKey:key] count]
You can always refer to the UITableView programming guide at http://developer.apple.com/iphone/library/documentation/UserExperience/Conceptual/TableView_iPhone/Introduction/Introduction.html
Hope that helps
Simply add a property to your custom UITableViewCell (depending on implementation) class that contains an int, NSNumber, or an NSIndexPath specifying which one it is. In you're using a data structure instead, then put it in you element in that data structure. Then you simply set the property when you create the data structure, something like elt.id=i, and then you access it in the cellForRowAtIndexPath, something like if (elt.id == 0 || elt.id == n-1) where n is the number of rows in your section.
I might have totally missed your question, but if I did, just comment and I'll post again.

iPhone SDK: Inserting and updating a UITableView with a new row

I have a tableView that needs to be updated after information has been inserted from another view. If I perform a
[self.tableView reloadData];
The very next time I insert more information in another view and try to reload the table, all the currently visible rows are duplicated.
In other words, when I start up the app I have:
tableView:
Row 1
Row 2
Then I submit some information that will also show up in the table and suddenly I have:
tableView
Row 1
Row 2
Row 3 <- info I just added
Row 1
Row 2
My numberOfRowsInSection implementation looks like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [ItemsController sharedItemsController].count;
}
My cellForRowAtIndexPath implementation looks like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ItemsController* controller = [ItemsController sharedItemsController];
NSMutableArray* recentItems = controller.listOfRecentItems;
CustomCell *cell = nil;
NSUInteger row = [indexPath row];
if( row < recentItems.count )
{
Items* item = [recentItems objectAtIndex:row];
if( recentCellData == nil )
recentCellData = [[NSMutableDictionary alloc] initWithCapacity:[indexPath length]];
if( [recentCellData count] > 0 )
cell = [recentCellData objectForKey:[NSString stringWithFormat:#"%d", row]];
if (cell == nil) {
UIViewController * view1 = [[UIViewController alloc] initWithNibName:#"CustomCell" bundle:nil];
cell = (CustomCell*)[view1 view];
[recentCellData setObject:cell forKey:[NSString stringWithFormat:#"%d",row]];
}
// do some other stuff here
}
// Set up the cell
return cell;
}
What's the best way to update the table and avoid duplicating the currently visible rows.
Thank in advance for all the help!
The error isn't in how you're reloading the table, it's in how you're providing data to it. Set a breakpoint in the data source methods and the method that adds new rows to see where you're going wrong.
You'll only end up with five items if tableView:numberOfRowsinSection: returns 5. Thats the simple answer to your question, but I see other problems here. I'm wondering why you have this test: row < recentItems.count. Is that array the same thing as [ItemsController sharedItemsController].count? You really need to be using the same array for both methods.
(Also, it's not a syntax error, but you shouldn't use the property syntax for things that aren't declared as properties. You should write [recentItems count] instead.)
I'm also confused by the code you use to set up the cell. Cells are meant to be reusable. That is, you create one cell, then reconfigure it every time in your implementation of tableView:cellForRowAtIndexPath:. Your code creates a cell for each item in your list. This is very memory-inefficient, and will likely crash your program due to insufficient memory on the iPhone if you keep lots of cells in memory like this.
The recommended approach is to call dequeueReusableCellWithIdentifier:. If that returns nil, then you set up a cell using the initWithFrame:reuseIdentifier: initializer. The table view is very smart, and will only ask you to redraw the cell when it needs you to.
Your recentCellData dictionary looks really shaky to me, too. What if you insert an item after the item with key #"2"? All the items with key #"3" onward will need to be shifted one element to the right to work the way you expect. That's a ton of bookkeeping that seems rather unnecessary to me. If you really needed something like this -- and to be clear, I don't think you do -- why wouldn't you use an NSMutableArray, which is much easier to use?
I added a bit more info above.