Why am I getting an infinite loop with my UITableView? - iphone

I'm calling a method that selects the first row of the table view when the view loads. But for some reason, after the selectFirstRow is being called, it goes back to self.couldNotLoadData = NO and keeps going back and forth. Any ideas why? When the initial if/else loop goes to else, that method isn't called so it doesn't keep looping.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section
{
if (self.ichronoAppointments.count > 0)
{
self.couldNotLoadData = NO;
[self selectFirstRow];
return self.ichronoAppointments.count;
}
else
{
self.couldNotLoadData = YES;
return 1;
}
}
-(void)selectFirstRow
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
}

This is unconfirmed, but I bet that when you call selectRowAtIndexPath:animated:scrollPosition: from selectFirstRow it calls UITableView's delegate's -tableView:numberOfRowsInSection:.
You've got infinite recursion going on, basically. tableView:numberOfRowsInSection calls selectFirstRow which calls selectRowAtIndexPath:animated:scrollPosition: which calls tableView:numberOfRowsInSection ad infinitum.
You need to move your selectFirstRow call to viewDidAppear or viewWillAppear. tableView:numberOfRowsInSection: is no place to be doing anything complex... it's called VERY VERY often.
And while you're at it, move the logic that checks for number of items into selectFirstRow. i.e.
if (self.ichronoAppointments.count) {
//select the first row
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
} else {
//don't
NSLog(#"Couldn't select first row. Maybe the data is not yet loaded?");
}
It's more DRY/modular/cleaner that way.

Related

Change an image in a UITableViewCell without reloading cell

I'm writing an iPhone app with a UITableView as the primary user interface. Each section consists of two rows, a header and the body. When the user clicks on the header, I remove the second row by changing the numberOfRowsInSection value:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
cbwComponent *comp = [_componentController objectInListAtIndex:section];
if([comp hasContentsView] && !comp.contentsHidden){
return 2;
}else
return 1;
}
When the user selects the header, I'm using the following code:
comp.contentsHidden = YES;
[self.tableView beginUpdates];
NSArray *deleteIndexPaths = [[NSArray alloc] initWithObjects:[NSIndexPath indexPathForRow:1 inSection:indexPath.section], nil];
[self.tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
It's working great, with a nice smooth fade effect. The problem is, I'm trying to add an indicator in the header cell (row 0) that changes when it's clicked on. To change that image I have to refresh the top row as well as the second row, which makes the transition look bad (well, not nearly as smooth). Is there a way to change the image in a UITableViewCell without refreshing the cell?
Thanks
EDIT: I figured it out! You can maintain the smooth transition as long as you reload that first row before you make the change to the second row. It has to be called inside of [tableView beginUpdates];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[[NSArray alloc] initWithObjects:[NSIndexPath indexPathForRow:0 inSection:indexPath.section], nil] withRowAnimation:UITableViewRowAnimationNone];
...
[self.tableView endUpdates];
Did the trick.
You could also subclass a tableview cell and implement a view transition in it that can be called from your view controller. You could then call that without having to reload the cell.
[(YourCustomCell*)[tableView cellForRowAtIndexPath:indexPathOfYourCell] fadeInIndicator];

Set first cell to highlighted on view's load

I have a UISplitViewController. When the app launches initially, the detail view is showing the detail data for the first row. However, the cell in the table is not highlighted, since it hasn't been selected yet. How can I set it to selected by default when the app loads initially?
I added this to cellForRowAtIndexPath but its not highlighting. It highlights fine when I actually select a cell.
if (indexPath.row == 0)
{
cell.selected = YES;
}
-(void)selectFirstRow {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
// optional:
// [self tableView:myTable willSelectRowAtIndexPath:indexPath];
[myTable selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
// optional:
// [self tableView:myTable didSelectRowAtIndexPath:indexPath];
}
Then call [self selectFirstRow]; wherever you need to.

Getting reference of a UITextField which is on a UITableViewCell?

Actually i am using next and previous button for moving one to another cell and each cell has a textfield so when i am clicking on next button it moves me to the next cell and by getting this cell reference i can make the text field become first responder but when i am clicking on previous button it returns me no reference.
The code which i am using for next and previous is given below
- (IBAction)nextPrevious:(id)sender
{
NSIndexPath *indexPath ;
BOOL check = FALSE;
if([(UISegmentedControl *)sender selectedSegmentIndex] == 1){
if(sectionCount>=0 && sectionCount<8){
//for next button
check = TRUE;
sectionCount = sectionCount+1;
indexPath = [NSIndexPath indexPathForRow:0 inSection:sectionCount];
}
}else{
//for previous button
if(sectionCount>0 && sectionCount<=9){
check = TRUE;
sectionCount = sectionCount-1;
indexPath = [NSIndexPath indexPathForRow:0 inSection:sectionCount];
}
}
if(check == TRUE){
//[registrationTbl reloadData];
UITableViewCell *cell = [registrationTbl cellForRowAtIndexPath:indexPath];
for(UIView *view in cell.contentView.subviews){
if([view isKindOfClass:[UITextField class]]){
[(UITextField *)view becomeFirstResponder];
break;
}
}
[registrationTbl scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
// UITextField *field = (UITextField *) [cell.contentView viewWithTag:indexPath.section];
// [field becomeFirstResponder];
}
Any small suggestion will be much appreciated. Thanks in advance
The problem lies in the scrolling. When you scroll to the top of the next row, the previous row gets removed and reused for the last visible row, meaning that the method cellForRowAtIndexPath: will probably return null, as the cell is not currently available.
The quick&dirty fix would involve scrolling to Middle or a little displaced so the cell is still visible. The not-so-quick-nor-dirty would involve making a procedure that scrolls the table to make sure the cell is visible, and then when the scrolling stops, set the textfield it as the first responder.
(Edit) To explain a little more this last approach. Let's say that you add a new variable NSIndexPath *indexPathEditing. The delegate method tableView:cellForRowAtIndexPath: would have:
if (indexPathEditing && indexPathEditing.row == indexPath.row && indexPathEditing.section == && indexPath.section)
{
// Retrieve the textfield with its tag.
[(UITextField*)[cell viewWithTag:<#Whatever#>] becomeFirstResponder];
indexPathEditing = nil;
}
This means that if indexPathEditing is set, and the current row that is being loaded is visible, it will automatically set itself as the firstResponder.
Then, for example (in your nextPrevious: method), all you need to do is:
indexPathEditing = [NSIndexPath indexPathForRow:0 inSection:sectionCount];
[registrationTbl scrollToRowAtIndexPath:indexPathEditing
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
[registrationTbl reloadData];
The row will appear, the tableView:cellForRowAtIndexPath: called, and it will get automatically set as the firstResponder.
Also, notice that instead of doing a for with isKindOfClass, it's easier to set a tag number, and then retrieve the object with viewWithTag:, I incorporated this in the example.

reloadRowsAtIndexPaths:withRowAnimation: crashes my app

I got a strange problem with my UITableView: I use reloadRowsAtIndexPaths:withRowAnimation: to reload some specific rows, but the app crashes with an seemingly unrelated exception: NSInternalInconsistencyException - Attempt to delete more rows than exist in section.
My code looks like follows:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
When I replace that reloadRowsAtIndexPaths:withRowAnimation: message with a simple reloadData, it works perfectly.
Any ideas?
The problem is that you probably changed the number of items of your UITableView's data source. For example, you have added or removed some elements from/to the array or dictionary used in your implementation of the UITableViewDataSource protocol.
In that case, when you call reloadData, your UITableView is completely reloaded including the number of sections and the number of rows.
But when you call reloadRowsAtIndexPaths:withRowAnimation: these parameters are not reloaded. That causes the next problem: when you are trying to reload some cell, the UITableView checks the size of the datasource and sees that it has been changed. That results in a crash. This method can be used only when you want to reload the content view of the cell (for example, label has changed or you want to change its size).
Now if you want to remove/add cells from/to a UITableView you should use next approach:
Inform the UITableView that its size will be changed by calling method beginUpdates.
Inform about inserting new row(s) using method - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation.
Inform about removing row(s) using method - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation.
Inform the UITableView that its size has been changed by calling the method endUpdates.
I think the following code might work:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
I had this problem which was being caused by a block calling reloadRowsAtIndexPaths:withRowAnimation: and a parallel thread calling reloadData.
The crash was due to reloadRowsAtIndexPaths:withRowAnimation finding an empty table even though I'd sanity checked numberOfRowsInSection & numberOfSections.
I took the attitude that I don't really care if it causes an exception. A visual corruption I could live with as a user of the App than have the whole app crash out.
Here's my solution to this which I'm happy to share and would welcome constructive criticism. If there's a better solution I'm keen to hear it?
- (void) safeCellUpdate: (NSUInteger) section withRow : (NSUInteger) row {
// It's important to invoke reloadRowsAtIndexPaths implementation on main thread, as it wont work on non-UI thread
dispatch_async(dispatch_get_main_queue(), ^{
NSUInteger lastSection = [self.tableView numberOfSections];
if (lastSection == 0) {
return;
}
lastSection -= 1;
if (section > lastSection) {
return;
}
NSUInteger lastRowNumber = [self.tableView numberOfRowsInSection:section];
if (lastRowNumber == 0) {
return;
}
lastRowNumber -= 1;
if (row > lastRowNumber) {
return;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
#try {
if ([[self.tableView indexPathsForVisibleRows] indexOfObject:indexPath] == NSNotFound) {
// Cells not visible can be ignored
return;
}
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
#catch ( NSException *e ) {
// Don't really care if it doesn't work.
// It's just to refresh the view and if an exception occurs it's most likely that that is what's happening in parallel.
// Nothing needs done
return;
}
});
}
After many try, I found "reloadRowsAtIndexPaths" can be only used in certain places if only change the cell content not insert or delete cells. Not any place can use it, even you wrap it in
[self beginUpdates];
//reloadRowsAtIndexPaths
[self endUpdates];
The places I found that can use it are:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (IBAction) unwindToMealList: (UIStoryboardSegue *) sender
Any try from other places like call it from "viewDidLoad" or "viewDidAppear", either will not take effect (For the cell already loaded I mean, reload will not take effect) or cause exception.
So try to use "reloadRowsAtIndexPaths" only in those places.
You should check cell visibility before reload. Here is Swift 3 code:
let indexPath = IndexPath(row: offset, section: 0)
let isVisible = tableView.indexPathsForVisibleRows?.contains{$0 == indexPath}
if let v = isVisible, v == true {
tableView.reloadRows(at: [indexPath], with: .automatic)
}
I had the same issue. In my case; it was happening only if another view controller pop/pushed over existing table view controller and then[self.tableView reloadRowsAtIndexPaths] function is called.
reloadRowsAtIndexPaths call was hiding/showing different rows in a table view which is having over 30, visually complex, rows. As i try to fix the issue i found that if i slightly scroll the table view app wasn't crashing. Also it wasn't crashing if i don't hide a cell (by returning 0 as height)
To resolve the issue, i simply changed the "(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath" function and returned at least 0.01 as row height.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
....
return rowModel.height + 0.01; // Add 0.01 to work around the crash issue.
}
it solved the issue for me.
THIS IS OLD. DO NOT USE.
I just bumped into this issue when I was calling reloadRowsAtIndexPaths... in order to change the cell to an editing cell containing a UITextField. The error told me I was deleting all of the rows in the table. To solve the problem, I removed:
[self.tableView beginUpdates];
NSArray *reloadIndexPath = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:count inSection:section]];
[self.tableView reloadRowsAtIndexPaths:reloadIndexPath withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
and replaced it with
[self.tableView reloadData];
The app crashes because you have made some changes to your tableView. Either you have added or deleted some rows to the tableView. Hence when the view controller asks your model controller class for data, there is a mismatch in the indexPaths. Since the indexPaths have changed after modification.
So either you simply remove the call
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
or replace it with
[self.tableView reloadData];
Calling reloadData checks your number of sections, number of rows in each section and then reloads the whole thing.
If data count changes completely, then use reloadData else, there is three functions to do it.
When data count changes we use insertRows / deleteRows and when data count still the same use reloadRows.
Important! don't forget call beginUpdates and endUpdates between insertRows/deleteRows/reloadRows calls.

UITable cell selection in a SplitViewController

I have a UISplitViewController with a Table View for navigation. It's similar to the Mail app. When you click on a table view in portrait mode, the popup hides itself. When you click on the nav bar to get the popup back, the selected item no longer appears selected. How can make this item appear selected without re-selecting the item? (just like in the mail app)
In your viewDidLoad method, do you call
self.clearsSelectionOnViewWillAppear = NO; ?
This is how Xcode's SplitView template does it.
Do you have by any change a
[tableView deselectRowAtIndexPath:indexPath animated:YES];
in your didSelectRowAtIndexPath in the RootViewController ?
I've got a solution that works, but it's frustratingly hacky. I have to call selectRowAtIndexPath twice. It seems that cellForRowAtIndexPath is invalidating the selection made in viewWillAppear. It still needs to be called in viewDidAppear, however, so the view scrolls to the proper position before cellForRowAtIndexPath is called.
- (void)viewWillDisappear:(BOOL)animated
{
NSIndexPath *selected = [self.tableView indexPathForSelectedRow];
_selectedRow = selected.row;
}
- (void)viewDidAppear:(BOOL)animated
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_selectedRow inSection:0];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//initialize cell code here...
if (indexPath.row == _selectedRow) {
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle];
}
}
For your table view controller, is -viewWillAppear: called before the pop-up is displayed? If so, you could write it as so:
- (void)viewWillAppear:(BOOL)animated
{
[self.tableView selectRowAtIndexPath:<indexPath>
animated:animated
scrollPosition:UITableViewScrollPositionMiddle];
[super viewWillAppear:animated];
}
Obviously, replace <indexPath> with the proper index path and set the scroll position how you want it. You may also want to pass NO instead of animated to make it appear like it was selectd before the view appeared.