EXC_BAD_ACCESS error at viewwillappear using scrollToRowAtIndexPath - objective-c

I'm getting an EXC_BAD_ACCESS error when using scrollToRowAtIndexPath in the viewWillAppear method. I searched for solutions and saw some old posts recommending to set delegate and table to nil (see code below), however when I set that I simply dont get anything loaded in my tableview.
I should say that this is part of a chat application where I want to show the last message entered first. Many thanks for any assistance with this.
Here's my viewWillAppear:
-(void)viewWillAppear:(BOOL)animated {
[self.table reloadData];
int lastRowNumber = [self.table numberOfRowsInSection:0] - 1;
NSIndexPath* ip = [NSIndexPath indexPathForRow:lastRowNumber inSection:0];
//self.table.delegate = nil;
//self.table = nil;
[self.table scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
I should add that this code seems to work fine in other parts of my program, the only time I get the error is in the viewWillAppear method.

viewWillAppear: is too early to do any animation on view. Per Apple Documentation:
This method is called before the view controller's view is about to
be added to a view hierarchy and before any animations are configured
for showing the view. You can override this method to perform custom
tasks associated with displaying the view. For example, you might use
this method to change the orientation or style of the status bar to
coordinate with the orientation or style of the view being presented.
If you override this method, you must call super at some point in your
implementation.
So, you cannot add animations when even the view hierarchy is not set completely.

It may help you
NSIndexPath * lastIndex =[NSIndexPath indexPathForRow:yourContenetArray.count-1 inSection:0];
[self.yourTableView scrollToRowAtIndexPath:lastIndex atScrollPosition:UITableViewScrollPositionNone animated:YES];

Write your code in viewDidAppear method.As your table view is loaded
then after you should call the method scrollToRowAtIndexPath . Then
your animation will be performed.And if you want to use in
viewWillAppear , then You can try to reload your tableView and then
write code for scrollToRowAtIndexPath

You should probably use the viewDidLoad method for scrolling, and if your array count is 0 (empty array) it will throw an error. It is not possible to scroll to cell at index -1.

Related

Cocoa Touch: How can I update the table view everytime I tap on a tab of tabbar?

I'm using iOS 8 and Objective c.
If I do a change in the values in one view, I can't see the reflection on my other tabviews. But if I re-run the app, I can see the reflection on the other tabs. Sometimes it works, sometimes it doesn't. How can I make sure that when I tap on a tabview, it will be reload according to the database?
I'm not sure what you are trying in your code, but you can try to implement the viewDidAppear method with a reloadData in your tableView:
- (void)viewDidAppear:(BOOL)animated{
[self.tableView reloadData];
}
[tableView reloadData] is the one you are looking for , this UITableView method refreshes the table view by calling its delegate and data source methods again to get fresh data.
I solved it!
Here is my code:
"TheMaster" is the main tabview. Here selectedIndex can be 0,1,2.... according to the placement of the tab. The leftmost tab has the index 0, and after that 1,2, and so on.
UITabBarController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"TheMaster"];
controller.selectedIndex=0;
[self presentViewController:controller animated:YES completion:nil ];

Strange behavior on scroll UITableView

I have a single view with nested scroll and table view with this hierarchical tree:
(The table is loaded with some Huckleberry Finn rows taken from this example.)
On viewWillAppear I programmatically scroll the Table View at the last row doing
-(void)viewWillAppear:(BOOL)animated
{
[self.tableView reloadData];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:0]-1 inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
and everything works as expected, in both orientation. As you can see, the text is rightly truncated.
I've uploaded on Github my project so you can see it running. Download it here.
Now, if you add this line in the cellForRowAtIndexPath method:
cell.textLabel.numberOfLines=0;
the cells automatically will resize to embed and show the entire text label:
BUT, and this is the issue my dear friends, as you can see the table is not scrolled to the last row but 5-6 rows above. And I cannot understand why.
The same thing happens also if NumberOfLines is 2,3 and so on. Only a value of 1 can make the TableView move down to the very last row.
(FYI, I do not use [self.tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX)]; in order to scroll down the table because it throws an exception)
Maybe something is wrong with iOS8 ?
viewDidLayoutSubviews will do the job
I guess you use some autolayout or self sizing cells? In viewWillAppear the calculation is not finished. So you scroll to the bottom, but then thereafter autolayout resizes your cells what makes them expand in height.
For Details: Refer to apple documentation UIViewController
... "Your view controller can override this method to make changes after the view lays out its subviews. The default implementation of this method does nothing."
- (void)viewDidLayoutSubviews {
// your code here
}
I added this event with reloadSection method and now the table scroll to the end. Also, I changed "tableView" to "myTableView" since I read that is better to give a non-default name to the views (as Sapi says in his answer here).
- (void)viewDidLayoutSubviews {
[self.myTableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.myTableView numberOfRowsInSection:0]-1 inSection:0];
[self.myTableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
IMHO this thing is very Machiavellian, however.

Why does an empty tableView check the number of sections but a non-empty one does not?

I have set up a demo application with a simple UITableViewController with no contents, but an 'Add' button in the toolbar. This launches a modal view controller which is again empty other than a 'cancel' button. The cancel button just tells its delegate (the UITableViewController) to dismiss the modal.
I then added an NSLog statement in the UITableViewController's numberOfSectionsInTableView method.
Ordinarily, when the table view controller loads I see two calls to numberOfSectionsInTableView. When I open and dismiss the modal (which returns to the UITableViewController) I see no further calls to numberOfSectionsInTableView.
However, if I return 0 from numberOfSectionsInTableView, in addition to the two calls on display, I also see an additional numberOfSections call when the modal is dismissed.
This only happens when numberOfSectionsInTableView returns 0, and I have added no additional code to my project besides that mentioned. This is easily verifiable by setting up a couple of controllers as I've described and modifying the result from numberOfSectionsInTableView.
My questions:
Why is the UITableView calling numberOfSectionsInTableView on return from a modal view?
Why is it only doing this if numberOfSectionsInTableView returns 0?
In addition to numberOfSectionsInTableView, the UITableViewController is also calling cellForRowAtIndex: when the modal is dismissed. In fact, it is attempting to display the new contents of its dataSource. How am I meant to manually animate a row insertion if the first row added is going to already be updated automatically? Shouldn't it be left to me to make sure that my UITableView is consistent with its dataSource?
What property is the UITableViewController checking to know that there is one or more sections (and therefore ask my delegate how many sections)? It can't be numberOfSectionsInTableView itself, since I would see it called whenever I return from the modal, not only when numberOfSections = 0.
From UITableViewController docs:
When the table view is about to appear the first time it’s loaded, the
table-view controller reloads the table view’s data... The
UITableViewController class implements this in the superclass method
viewWillAppear:
If you watch in the debugger, the second call upon app launch is from UITableViewController's viewWillAppear: implementation - specifically the part referred to above, where tableView is sent the reloadData message.
Now, the first call to numberOfSectionsInTableView: on launch is also from UITableViewController's implementation of viewWillAppear: but not directly from that implementation's call to -[UITableView reloadData]. I'm not sure what the first call is all about.
But, to your question, the call to numberOfSectionsInTableView: that happens when dismissing the modal has exactly the same call stack as the second call from applicationDidFinishLaunching:withOptions:. My hypothesis then is that UITableView interprets having zero sections as being in a state where it has not loaded at all. That does make some sense actually. I'd consider an "empty" table view to be one without any rows, but one without any sections seems almost "uninitialized" to me. Furthermore the UITableViewDataSource documentation implies UITableView has by default one section. Returning zero from this method would be inconsistent with that assumption of the docs as well.
Now, to your concern about animation - if you give the table an empty section to work with, you will be able to have full control over inserting the first row with whatever animation you'd like, and not be locked in to when you need to reload.
I think the moral of the story is, don't return zero sections unless you really, really need to for some reason. The title of your post refers to this table view being "empty" as well but I think it's clear the framework finds zero sections to not be empty but unloaded.
Hope this helps! And thanks for posting the sample project for me to play around with.
Perhaps the delegate just couldn't believe its eyes. But seriously, since a table view has to have at least one section, passing 0 doesn't make any sense. Why do it? You pass it an invalid argument and it gives you back a weird response. As to why it doesn't ask for number of sections when you pass 1, I think it's because it doesn't need to know at that point (coming back from the modal view controller) -- the table view has already been populated (if there were some data) and you haven't changed anything in the model, so it doesn't need to update.
I added a few lines to your example project to slide in a row each time you return from the modal view controller, which is what I think you're trying to do. I added an int property num for the return value of numberOfRowsInSection, added an array to populate the table, and a call to insertRowsAtIndexPaths from the modal view controller dismissal method.
- (void)viewDidLoad
{
_num = 0;
self.theData = #[#"one",#"two",#"three"];
[super viewDidLoad];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:#selector(addRecipe)];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSLog(#"# sections requested");
//when 0, this fires on return from the modal. When 1, it does not.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"in numberOfRows in section");
return _num;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"in cellForRowAtIndexPath");
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil];
cell.textLabel.text = [self.theData objectAtIndex:indexPath.row];
return cell;
}
- (void)addRecipe
{
//create the modal and suscribe for delegate notifications
AddRecipeViewController *addRecipeController = [[AddRecipeViewController alloc]initWithStyle:UITableViewStyleGrouped];
addRecipeController.delegate = self;
//display the modal in a navigation controller
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:addRecipeController];
[self.navigationController presentModalViewController:navController animated:YES];
}
- (void)addRecipeVC:(AddRecipeViewController *)addRecipeVC didAddRecipe:(NSString *)recipe
{
[self dismissModalViewControllerAnimated:YES];
_num += 1;
[self performSelector:#selector(addRow) withObject:nil afterDelay:.5];
}
-(void)addRow {
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:_num-1 inSection:0]] withRowAnimation:UITableViewRowAnimationRight];
}
The tableview checks number of sections when it's populating the table view with data!
Since the table can be divided into sections, it has to know specifically how many sections to divide it into.
When you reload the data, the number of sections is also checked.
Because every time the table view has to take action in accessing either the data of the table, like what row you tapped, and in what section, or populating the data table, the number of sections has to be known!
Hope this helped!

Passing NSIndexPath to New View

I am having real troubles passing an NSIndexPath to my new view. This is how the app works:
I have a UIBarButtonItem in my nab br, tap that and you get a popover view, this shows a bunch of stuff. I need to get an NSIndexPath from my main view, to this popover view.
I have a property for the NSIndexPath in my popover view class and the popover transition is connected up in my storyboard.
Then I have this code to pass the index path across views:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"statsPopover"])
{
StatsViewController *statsVC = [segue destinationViewController];
statsVC.selectedIndex = stageSelectionTable.indexPathForSelectedRow;
}
}
However, while this gets called, the index path isn't actually sent between views. My index path on the popover is always the default, 0,0 row and section.
You say you know from debugging/logging that the method is running, your if statement is triggered, and stageSelectionTable.indexPathForSelectedRow has the value you expect.
Doing that kind of diagnosis puts you on the right path to solving your issue. Keep at it -- if you test some other things with NSLog or the debugger you should be able to find the problem.
Do you know that statsVC is non-nil? If it's nil, your message to set selectedIndex does nothing.
Is statsVC the class you expect? If, say, the (table?) view in your popover is embedded in a navigation controller, segue.destinationViewController will point to the nav controller, and you'll need to look into its child view controllers array to find the one you're looking for.
Is whatever accessor method your StatsViewController class is using for its selectedIndex property working right? (This shouldn't be a problem if it's a synthesized accessor for a strong/retain property.)
In your StatsViewController class, are you trying to work with selectedIndex before it actually gets set? Setting a property in prepareForSegue:sender: and then using it in viewDidLoad should work fine, but using it in awakeFromNib might not, and using it in an init... method definitely won't.

Insert rows in UITableView from code

This seems ridiculous but I can't seem to sort it out. I need to insert a row in a table view in response to a push notification. The implementation should be as simple as
- (void)didSaveMessage:(Message*)message atIndex:(int)index
{
//check to make sure I have an array to populate the table from...
if (self.messageManager.messages != nil)
{
self.indexRow = index;
NSLog(#"refresh table view from code");
//remove a label that says "You have no messages"...
[[messageTable tableFooterView] removeFromSuperview];
[messageTable beginUpdates];
[messageTable insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:index inSection:0]]
withRowAnimation:UITableViewRowAnimationFade];
[messageTable endUpdates];
[self scrollToNewestMessage];
}
}
This method is called when I receive a remote notification and it does fire. The log statement prints. However, the UITableView does not update. What is particularly strange to me is that if I put the same code in an IBAction and link it to a refresh button in storyboard, it works. Is there something special that I have to do to update a UITableView programmatically?
Try to send the table the reloadData message. This should force the table view to update.
It seems pretty trivial, but is it possible that the Message* that you are passing into the didSaveMessage is nil? It would explain why the NSLog statement works, and I would assume that you had to create the message manually when testing from an IBAction.
This is resolved. I was calling the method to update the UI from didReceiveRemoteNotification: in the AppDelegate but to a different instance of my view controller. So I added a viewController property onto the app delegate, set the value for that in my view controller's view did load, and now it's all working.