I am implementing an a UITableView and have the - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath method implemented and within that method I have if loops (below):
NSInteger row = [indexPath row];
if (self.someDetailViewController == nil) {
if (row==0) {
OneTableViewController *aDetail = [[OneTableViewController alloc] initWithNibName:#"OneDetail" bundle:nil];
self.oneDetailViewController = aDetail;
[aDetail release];
} else if (row==1) {
OneTableViewController *aDetail = [[OneTableViewController alloc] initWithNibName:#"TwoDetail" bundle:nil];
self.oneDetailViewController = aDetail;
[aDetail release];
}
}
However each time I select something in my table view (let's say row 0) I am taken to the secondary view (OneDetail) and then when I go back and select another row (row 1) and I expect to go to the other view (TwoDetail) however I am taken to OneDetail (to original row that was selected first) - how can this be when the user taps another row they are taken to the first row's secondary view that was originally tapped. This also happens vice versa (i.e. selecting row 1 and being taken to TwoDetail and then going back and selecting row 0 and also being taken to TwoDetail not OneDetail...
I was wondering if anyone knew how to 'restart' an if loop when the user presses the back button or how to overcome my issue in some other fashion. Thanks so much in advance!
I suspect that the second time you enter the method, this check is returning false:
if (self.someDetailViewController == nil)
Thus, you never get into the part where you check which row you're on, and you're permanently stuck with whichever one you set first.
This is not a loop.
You store your detail view controller in the instance variable oneDetailViewController.
Only if that someDetailViewController is nil, which is the case most probably only when the method is executed for the first time, you will assign a value.
A view controller that is initialized with "oneDetail".
Unfortunately you do not show the remaining code to us. I assume that you do not have a statement
self.someDetailViewController = nil;
further down in the method.
Why do yo do that == nil thing anyway? What is the detail view controller good for in the further processing? Just release it at the end of didSelectRowAtIndexPath and create a new one next time it is executed. There is no need for an instance variable saving it.
Hoever, if you have good reasons for this unusual piece of code, which you did not share with us, then we could certainly make a suggestion on how to achieve that - if you let us know :)
Related
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 */
}
I am using the TableViewUpdates example from WWDC #2010. Basically Apple creates collapsable and expandable TableViews by clicking on the section header. The data for the TableView gets created in viewWillAppear like so:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
/*
Check whether the section info array has been created, and if so whether the section count still matches the current section count. In general, you need to keep the section info synchronized with the rows and section. If you support editing in the table view, you need to appropriately update the section info during editing operations.
*/
if ((self.sectionInfoArray == nil) || ([self.sectionInfoArray count] != [self numberOfSectionsInTableView:self.tableView])) {
// For each play, set up a corresponding SectionInfo object to contain the default height for each row.
NSMutableArray *infoArray = [[NSMutableArray alloc] init];
for (Play *play in self.plays) {
SectionInfo *sectionInfo = [[SectionInfo alloc] init];
sectionInfo.play = play;
sectionInfo.open = NO;
NSNumber *defaultRowHeight = [NSNumber numberWithInteger:DEFAULT_ROW_HEIGHT];
NSInteger countOfQuotations = [[sectionInfo.play quotations] count];
for (NSInteger i = 0; i < countOfQuotations; i++) {
[sectionInfo insertObject:defaultRowHeight inRowHeightsAtIndex:i];
}
[infoArray addObject:sectionInfo];
[sectionInfo release];
}
self.sectionInfoArray = infoArray;
[infoArray release];
}
}
I've noticed for my case, where I have a lot of data, this is an expensive operation. I'd like to cache the data. The data gets created each time since it's in viewWillAppear. Because I'm using a UINavigationController to push this view onto the stack, if I put it into viewDidLoad, when I move away from this view and go back to home, I have to recreate the view again, viewDidLoad will run again, and it'll be slow again.
I haven't cached data before and was wondering what a good way to do it would be? Right now all of the data for the row headers and rows are in a database. So when this view gets pushed onto the stack, I grab the data, and create the table. I didn't know what a good mechanism would be to create the table and somehow cache the view or something to make it load faster on subsequent pushes of the viewController. Thanks.
The code you are showing is constructing the data source for the table view, and is not part of the view itself, per se. Wouldn't it meet your needs to execute this code in the view controller initializer and/or whenever your data source requires an update?
You can draw the parallel with NSFetchedResultsController and its delegate methods. These are executed apart form the view handling methods, with the fetched results controller being an ivar or (very often) a property of your view controller. Then, for example, once a fetched results controller has done its fetch, it can manage changes on a case-by-case basis, coordinated with the table view and its controller, or you can purposely refetch entirely. The view can appear and disappear completely independent of the data source maintenace.
I've discovered a strange behavior with setSelected:animated: in my custom UITableViewCell class. I discovered that this function gets called multiple times if I click on a cell in my table. I am wondering if this is normal behavior or a bug in my code.
To help with debugging, I've modified the setSelected:animated: function in my custom UITableViewCell class implementation as such:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state.
if (selected)
NSLog(#"Yes %X", &self);
else
NSLog(#"No %X", &self);
}
If I click on a cell in the simulator, here is what I get in the console:
2011-03-22 22:05:26.963 marketPulse[3294:207] Yes BFFFDDD0
2011-03-22 22:05:26.964 marketPulse[3294:207] Yes BFFFDE30
You would think that I would get only 1 entry, since I only clicked on 1 cell.
And if I click on a different cell after that:
2011-03-22 22:07:11.014 marketPulse[3294:207] No BFFFD890
2011-03-22 22:07:11.016 marketPulse[3294:207] No BFFFDD00
2011-03-22 22:07:11.017 marketPulse[3294:207] Yes BFFFDDD0
2011-03-22 22:07:11.017 marketPulse[3294:207] Yes BFFFDE30
If I click on the same cell 2 times in a row, I get more than 2 Yes:
2011-03-22 22:08:41.067 marketPulse[3294:207] Yes BFFFDDD0
2011-03-22 22:08:41.068 marketPulse[3294:207] Yes BFFFDE30
2011-03-22 22:08:41.069 marketPulse[3294:207] Yes BFFFDE30
The more times I click the same cell, the more Yes I will get, and if I click on a different cell after that, I'll get a lot of No
I put a breakpoint before the NSLog, and looking at the debugger, it seems that all the repeated calls are coming from the same object.
Here is a part of my tableView:cellForRowAtIndexPath: function so you can see how my cells are being treated:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *ContentCellIdentifier = #"newsTableCellContent";
UITableViewCell *cell;
//index of cell data in tableData
NSUInteger index = indexPath.row / 2;
...
//content of story
else if( [indexPath row] % 2 == 1 ) {
cell = [tableView dequeueReusableCellWithIdentifier:ContentCellIdentifier];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle]
loadNibNamed:#"newsTableCells"
owner:nil options:nil];
for (id currentObject in topLevelObjects) {
if ( [currentObject isKindOfClass:[newsTableCellContent class]] ) {
cell = currentObject;
break;
}
}
}
((newsTableCellContent *)cell).content.text = [[tableData objectAtIndex:index] description];
}
return cell;
}
Everything works fine so its hard to tell if the repeat calls to setSelected:animated: are intentional or not. If this is normal operation, I can make do with another method, but I would just like to know if this is suppose to happen or not.
Thanks
What's going on is simply that the UITableView keeps track of which cells are selected in the table.
Since cells are reused when you scroll through a large table view, the table view has to keep the list of selected cells separate. Not only that, but whenever it reuses a cell it has to set its selected property, because it may be using an old, invalid selected state from a previous incarnation.
When you tap a cell, several things happen: the previously selected cell is deselected (using setSelected:). The new cell is highlighted. It's de-highlighted (at least if you tap, instead of holding your finger down), and the setSelected: method is called because the new cell was selected. That's one.
The second call is a delayed perform call, possibly from a point where the table view didn't yet know what the final state of the table would be. This call goes to _selectAllSelectedRows, which, as the name suggests, calls 'setSelected:animated:' on all selected rows. That's the second call. The reason for this is most likely to address potential issues due to the the table view being in a "transition", but who knows.
Whether it's a bug or not is up for interpretation. A fix for the duplicate calls is to simply do:
if (self.selected == selected) return;
right before the call to super (you do not have to call super if self.selected == selected).
This is a normal behavior if you're using iPad. (it is only called once on iPhone).
In order to stop getting multiple "setSelected:YES" or multiple "setSelected:NO", all you have to do is this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Now, 1 click on any cell gives you:
1 entry of setSelected:YES animated:NO
1 entry of tableView: didSelectRowAtIndexPath:
1 entry of setSelected:NO animated:YES
So, calls are now stable regardless of what you do.
Ideally you should not be calling setSelected from anywhere in your code.
UIKit will take care of calling it.
If you want to show a cell/row as selected in cellForRowAtIndexPath method simply call
tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: .None)
for that specific indexPath.
Again never ever call setSelected explicitly unless you really mean to.
It should definitely be called when table is scrolled. Cells are reused, that means, if you scroll cells in invisible areas will be reused and reinitialized, including the call to setSelected, which is basically a lightweight property setter.
If you really want to see what's happening, add a NSLog to tableView:cellForRowAtIndexPath: which will log indexPath and the returned cell.
The entire log should give you a good understanding what happens inside and why.
I suppose it will be something like this (Clicked on IndexPath 1:1)
Give me cell on 1:0 (previously selected cell).
Deselect 1:0
Give me cell on 1:0 again (updated after deselection)
Deselect 1:0 (update selected flag on this cell and trigger animation)
Give me cell on 1:1
Select 1:1
Give me cell on 1:1 again (updated after selection)
Select 1:1 (update selected flag on this cell and trigger animation)
Clicking on a selected cell again is only slightly different - instead of triggering unselecting, it triggers another update.
In my 'Sectioned' UITableView I have two sections, the first one for Attributes like name and the second one is editable where you can add objects.
Here's a picture of it:
alt text http://fwdr.org/bm9w
There's an attribute that the user can change (in this case Type) which would to change the number of rows in second section. To be more specific, if one property is selected the maximum number of rows is 2 and then there would be no Add New… row.
Here's what I've tried (in my UITableViewController) …
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([self isToManyRelationshipSection:section]) {
NSArray *sectionKeys = [rowKeys objectAtIndex:section];
NSString *row0Key = [sectionKeys objectAtIndex:0];
if ([[NSString stringWithFormat:#"%#", [managedObject valueForKey:#"type"]]
isEqualToString:[NSString stringWithFormat:#"One on One"]] && [[managedObject valueForKey:row0Key] count] == 2){
return [[managedObject valueForKey:row0Key] count];
}
return [[managedObject valueForKey:row0Key] count] +1;
}
return [rowLabels countOfNestedArray:section];
}
But if the user tries to remove a row when there are already 2 rows the app crashes because there would be the same number of rows before and after the deletion.
How do I get around this and do this properly?
First off, the numberOfRows.. method wouldn't appear to me as the proper place to do this kind of logic. I'd rather create a seperate method to determine and return the current game mode (and keep the gamemode updated whenever the user selects another mode). I'd associate integers with gamemodes (1 = "one on one", 2 = "all against all"...), that way in the numberOfRows Method you just need to check "is the current section == 1?" && "which game mode are we in?" to determine how many rows are needed.
If you keep track of the gamemode properly, you don't need to check for equal strings anymore, too.
I ended up doing this differently to what I first wanted to do.
My reasoning was that my current implementation gave me an error in the debugger which in turn crashed the app. This error seemed un-avoidable so I decided to simply disable the cell if there are two rows, instead of hiding it.
To do this in my tableView:cellForRowAtIndexPath: method I added the following code (in my if statement) to make it look un-selectable to the user:
cell.textLabel.textColor = [UIColor lightGrayColor];
cell.editingAccessoryType = UITableViewCellAccessoryNone;
And in the tableView:didSelectRowAtIndexPath: method I left my if statement blank so it wouldn't push another view controller or add a object. Although I did include this code to deselect the row:
[self.tableView deselectRowAtIndexPath:indexPath animated:NO];
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.