I have a class that allows a user to add entries to a server-side table. Everything works correctly until I attempt to refresh the UITableView with the new data. I make a server call to get the new dataset, use it to refresh the NSArray that is the data source for the table, and then attempt to reload the table. Here is the method that is called when the data comes back from the server:
- (void) logEntriesRefreshed : (NSNotification *) notification {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:#"log_entries_refreshed"
object:nil];
NSLog(#"returned from log entries fetch");
_logEntriesArray = [LogEntriesDataFetcher getLogEntriesArray];
[_tableView reloadData];
_activityIndicator.hidden = YES;
[_activityIndicator stopAnimating];
NSLog(#"log entries array count: %lu", [_logEntriesArray count]);
[_tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
It's this last line that is the problem. I want to programmatically select the first row in the table (there has to be at least one, since I just added a row). But it appears that this line never executes. Note this method, which should go next:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"here");
UITableViewCell *previousCell = (UITableViewCell *)[_tableView cellForRowAtIndexPath:_previousIndexPath];
previousCell.backgroundColor = [UIColor clearColor];
previousCell.textLabel.textColor = [SharedVisualElements primaryFontColor];
UITableViewCell *cell = (UITableViewCell *)[_tableView cellForRowAtIndexPath:indexPath];
cell.contentView.backgroundColor = [SharedVisualElements secondaryFontColor];
cell.textLabel.textColor = [SharedVisualElements primaryFontColor];
_previousIndexPath = indexPath;
// get the file attributes for the cell just selected
_currentEntry = (LogEntry *)[_logEntriesArray objectAtIndex:[indexPath row]];
NSLog(#"array count: %lu", (unsigned long)[_logEntriesArray count]);
NSLog(#"current entry: %ld", (long)[indexPath row]);
_isExistingEntry = YES;
_arrayPositionOfEntryBeingEdited = [indexPath row];
[self initializeValues];
[self initializeObjects];
[self captureStartingValuesForStateMachine];
}
I have break points set on the selectRowAtIndexPath line and also on the first NSLog(#"here") line in didSelectRow.... I get to the selectRowAtIndexPath line but never to the didSelectRow method. My console output is consistent with that:
returned from log entries fetch
log entries array count: 7
and that is the end of it. Nothing from the didSelectRow... method. There are no errors thrown, either.
What am I missing. Seems pretty straightforward, but nothing I do seems to work.
As per Apple's documentation, calling selectRowAtIndexPath will NOT invoke the didSelectRowAtIndexPath. Take a look here.
Calling this method does not cause the delegate to receive a
tableView:willSelectRowAtIndexPath: or
tableView:didSelectRowAtIndexPath: message, nor does it send
UITableViewSelectionDidChangeNotification notifications to observers.
To specifically invoke the didSelectRowAtIndexPath delegate method, use the following code:
[[tableView delegate] tableView:tableView didSelectRowAtIndexPath:indexPath];
Hope this helps.
Related
I've got my cellForRowAtIndexPath delegate method defined as so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PLOTCheckinTableViewCell *cell = (PLOTCheckinTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CheckinCellIdentifier forIndexPath:indexPath];
if([self.items count] == 0){
return cell;
}
NSDictionary *checkin = self.items[indexPath.row];
// configure and return custom cell
}
I'm using a custom cell class (PLOTCheckinTableViewCell).
I faced an issue where the user would pull to refresh and then attempt to pull again before the first request had completed (on completion of the request, I reload the table data). When they did this, the app would crash and say that indexPath.row was basically out of bounds, ie. the array was empty.
By putting in this IF check above, I mitigated the crash.
However,
Why exactly does my IF check "work", I see no visual implications of returning the cell before it's been configured. This is confusing
Are there any better ways to guard against this happening (ie. the table data being reloaded with an empty array)? Surely the numberOfRowsInSection would have returned array count which would be 0? (if it was an empty array)
EDIT (further code)
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
float count = [self.items count];
return count;
}
- (void)resetData {
self.items = [NSMutableArray array];
}
-(void) refreshInvoked:(id)sender forState:(UIControlState)state {
[self resetData];
[self downloadHomeTimeline];
[self.refreshControl endRefreshing];
}
- (void)downloadHomeTimeline {
[self.apiClient homeTimeline:self.page completionBlock:^(NSDictionary *data){
for (NSDictionary *obj in data[#"items"]) {
[self.items addObject:obj];
}
[self.itemsTableView reloadData];
}];
}
I couple of things that i would suggest to do. Make sure that the [self.itemsTableView reloadData] is executed on the main thread and also i would put the [self.refresControl endRefreshing] in the completion block. This way it will stop the refresh when its done and you should not let the user more then once simultaneously.
- (void)downloadHomeTimeline {
[self.apiClient homeTimeline:self.page completionBlock:^(NSDictionary *data){
for (NSDictionary *obj in data[#"items"]) {
[self.items addObject:obj];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.itemsTableView reloadData];
[self.refreshControl endRefreshing];
});
}];
}
Also in the numberOfRowsInSection just return count
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.items count];
}
To add to the answer. You should not reset the array before you receive new data. While getting new data the user can still scroll the table and that means new cells will be created but your NSMutableArray doesn't have any data. That is when you get the error and app crashes. You would have to [tableView reloadData] to clear the table so that the tableView would know that there are 0 rows, which i don't think is your intent.
Let me know if that's solves the issue.
I am a beginner in Objective-C & iPhone development.
I add dynamically cells in a TableView. I want to set labels's text properties with an array. I saw many tutorials, and I searched during several hours but labels are never filled.
My code is :
- (void)insertNewObject
{
for (NSInteger ic=0; ic<((pages.count)); ++ic) {
NSLog(#"%d", ic);
NSDictionary *monDico = pages[ic];
menu = [monDico objectForKey:#"Name"];
NSIndexPath *indexPathTable = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPathTable] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; // I try include & exclude : never call
}
[self.tableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = menu[indexPath.row];
NSLog(#"%#%#", #"Cell Label = ", cell.textLabel.text);
return cell;
}
Please note that insertNewObject method is called during viewDidLoad execution.
I use a breakpoint in the cellForRowAtIndexPath method : it never calls ! I try with :
explicit calling
forcing reloadData method
but did not work too.
Can you please tell me why ?
Thanks in advance.
If cellForRowAtIndexPath is not being called then most likely you have not set:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [menu count]; // indicates the number of rows in your table view
}
This method needs to return the number of rows you expect to render within your table view. The default is 0 = no rows. I'm assuming you want to show all the items in your menu array so simply return [menu count].
Check this: UITableViewDataSource Protocol Reference
If you want to access to textLabel property of your cell, then it must be style of: UITableViewCellStyleDefault. Or, if you use storyboard, then set Cell's style to Basic.
And, of course, make sure that you have set delegate and datasource properties of your tableView.
- (void)viewDidLoad
{
[super viewDidLoad];
//...
self.tableView.delegate = self;
self.tableView.dataSource = self;
}
------------------EDIT------------------
If you're using UITableViewController, then no more need to set delegate and dataSource properties manually, because they will automatically set by UITableViewController when your view did load.
If cellForRowAtIndexPath: method still not being called, then make sure that following methods that you implemented, both returns value >0:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
and
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
Set a breakpoint before return and see returning values, or just NSLog them before returning.
Using this code
- (IBAction)testAdd:(id)sender
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows inSection:0];
[self.tableView beginUpdates];
self.numberOfRows++;
[self.tableView insertRowsAtIndexPaths:#[indexPath]withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
I'm able to add a new item to a tableView via an 'add' button on the app. This basically adds an item identical to the item already on the table that preceded it.
For example, I have a tableview with the first row displaying a string "TEST", hitting add adds another row that displays "TEST".
I would like to be able to pass in a custom value for the new row, so hitting add outputs a row with say "NEWTHING".
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = self.val2;
return cell;
}
My data source is actually another view controller that takes user inputs and sends it to my tabelViewController, with the text for the item as "val2".
What I actually want to achieve is the ability to hit add, go back to the user input view controller, get the new data and send it back to my tableViewController to be displayed
What you're asking, is the kinda stuff that is to be done in -cellForRowAtIndexPath: (most of the times, it depends on the way you have designed your datasource) but if it doesn't matter to you, then you can do:
- (IBAction)testAdd:(id)sender
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows
inSection:0];
self.numberOfRows++;
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell.textLabel setText:#"NEWTHING"];
}
But note that when you scroll far up/down and return to this cell, it will most probably show "TEST" (that's where -cellForRowAtIndexPath: will show it's true purpose)
PS: Include your -cellForRowAtIndexPath: method implementation in the question if you want to proceed further
EDIT:
Your -cellForRowAtIndexPath is too static... in the sense that it simply sets self.val2 to cell.textLabel.
Lets say you start with 10 rows, -cellForRowAtIndexPath will be called 10 times and every time, it will set self.val2 onto the current cell's textLabel.
Now... when you add one row (on a button tap), the -cellForRowAtIndexPath will be called for the 11th cell and the same* text will be set to it.
*this technically happened but we quickly changed the cell's text
Basically, the tableView doesn't know how to differentiate between an existing cell and a new added cell because the datasource itself is not dynamic.
To direct the tableView on how to handle different cells, we need to create a more dynamic datasource.
There are different approaches use but I'd generally do it this way:
- (void)viewDidLoad
{
[super viewDidLoad];
self.val2 = #"TEST";
//declare "NSMutableArray *arrDatasource;" globally
//this will be the soul of the tableView
arrDatasource = [[NSMutableArray alloc] init];
int i_numberOfCells = 10;
//populate beginning cells with default text
for (int i = 0; i < i_numberOfCells; i++) {
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:self.val2 forKey:#"displayText"];
[arrDatasource addObject:dictionary];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//return number of objects in arrDatasource
return arrDatasource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
//pick up value for key "displayText" and set it onto the cell's label
[cell.textLabel setText:arrDatasource[indexPath.row][#"displayText"]];
//this will be dynamic in nature because you can modify the contents
//of arrDatasource and simply tell tableView to update appropriately
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//make indexPath of new cell to be created
NSIndexPath *indexPathNEXT = [NSIndexPath indexPathForItem:arrDatasource.count inSection:0];
//add the appropriate contents to a dictionary
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:#"NEWTHING" forKey:#"displayText"];
//add the dictionary object to the main array which is the datasource
[arrDatasource addObject:dictionary];
//add it to tableView
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPathNEXT]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
//this ends up calling -cellForRowAtIndexPath for the newly created cell
//-cellForRowAtIndexPath shows the text (you put in the dictionary in this method above)
}
PS: -cellForRowAtIndexPath: is called whenever cell updates or refreshes or needs to be displayed and so this method needs to be implemented properly
I have to select one by one cell in a tableview and "execute" this row to show their detailviewcontroller. I use the following code. Everything is working as it should be, as long as the next row to be executed is visible in the screen. Is the next cell in the red zone the user has to manually scroll to this cell and than it get's executed.
Any help
- (void)showNextExercise:(NSNumber *)nextOne{
int nextRow = [(NSNumber *)nextOne intValue];
NSLog(#"Next Row: %i", nextRow);
//Auto Start next Exercise //
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:nextRow inSection:0];
if ([self.tableView.delegate respondsToSelector:#selector(tableView:willSelectRowAtIndexPath:)]) {
[self.tableView.delegate tableView:self.tableView willSelectRowAtIndexPath:indexPath];
}
//[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition: UITableViewScrollPositionNone];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
if ([self.tableView.delegate respondsToSelector:#selector(tableView:didSelectRowAtIndexPath:)]) {
[self.tableView.delegate tableView:self.tableView didSelectRowAtIndexPath:indexPath];
}
[self performSelector:#selector(performSelectorWithDelay) withObject:nil afterDelay:0.2];
//END Auto Start next Exercise //
}
-(void)performSelectorWithDelay{
[self performSegueWithIdentifier:#"showDetail" sender:nil];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
if (![executedExercises containsObject:[NSString stringWithFormat:#"%#", [[object valueForKey:#"timeStamp"] description]]]) {
[executedExercises addObject:[NSString stringWithFormat:#"%#",[[object valueForKey:#"timeStamp"] description]]];
}
[autoExecutedExercises addObject:[NSString stringWithFormat:#"%#",[[object valueForKey:#"timeStamp"] description]]];
//[autoExecutedExercises addObject:[NSNumber numberWithLong:indexPath.row]];
if (indexPath.row == 0) {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
long warmUpTime = [userDefaults integerForKey:#"WarmUpBackup"];
NSLog(#"warmupBackUp: %li", warmUpTime);
}
}
Red Zone Image
What did you do exactly in tableView:didSelectRowAtIndexPath? Due to the reuse cell mechanism, invisible cells may not exist if you try to get them in tableView:didSelectRowAtIndexPath.
Don't abuse the UI system. You don't need to tell the table view delegate that a cell is going to be and then has been tapped on, you can just present the view controllers you require directly (call the method that presents without bothering the delegate).
You have the data that goes into the table view and you should be using that directly. Funnelling everything through the UI code is a burden and an application of knowledge and cross coupling in the code where there should be none.
If you wanted to show the table rows as selected, set a flag for the selected item in the data model (band scroll / reload the table). Now when the table is refreshed you can mark the cell.
I have a feature in my app where the user can change the color scheme of the app. The app uses a Split View Controller, with a MainTable and DetailView table. Everything works fine except for the MainTable. What is failing is that the MainTable reloadData method is not causing the cells to be redrawn.
It should be noted that I am changing globalHighContrast and sending the notification from a UIModalPresentationFormSheet viewController, so the tables are kind of visible on the screen while the viewController is active.
I am triggering the screen update from a notification, like this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadAllTables)
name:#"contrastModeChanged"
object:nil];
Then, to make sure that I call reloadData on the main thread, I am handling the notification like this:
-(void)reloadAllTables{
[self performSelectorOnMainThread:#selector(doReloadAllTables) withObject:nil waitUntilDone:NO];
}
-(void)doReloadAllTables{
[self showIcon];
if( globalHighContrast ){
theTable.backgroundColor = [Colors lightBkgColor];
self.view.backgroundColor = [Colors lightBkgColor];
} else {
theTable.backgroundColor = [Colors darkBkgColor];
self.view.backgroundColor = [Colors darkBkgColor];
}
[detailViewController configureView:currentMainMenu];
[detailViewController.subTable reloadData];
[theTable reloadData];
// desperate try to force it to work
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentMainMenu inSection:0];
[self tableView:theTable didSelectRowAtIndexPath:indexPath];
}
Both reloadAllTables and doReloadAllTables are being called, but
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
is not being called.
As soon as I tap a cell on the MainTable it does update correctly to the new color scheme.
Also, there is a desperate attempt to workaround this by trying to simulate the MainTable touch, but that doesn't work either.
You can try to put code for updating you scheme in -(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method...