Update values inside cell after NSFetchedResultsChangeUpdate - objective-c

I have a tableview and I link it to core data using NSFetchedResultsController. I have a method updateCellAtIndexPath, that I am using to update cell text label. I call this method from tableView:cellForRowAtIndexPath:indexPath. That works fine.
-(UITableViewCell *)updateCellAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"TripCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
Trip *trip = [_controller objectAtIndexPath:indexPath];
cell.textLabel.text= [NSString stringWithFormat:#"%#" ,trip.name ];
return cell;
}
When I update core data object I receive NSFetchedResultsChangeUpdate and I call updateCellAtIndexPath. The problem is that the cell text label is not updated correctly to actual value. I think the cell is not reloaded, because everything works if I change updateCellAtIndexPath to reloadRowsAtIndexPaths:withRowAnimation:
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch (type){
case NSFetchedResultsChangeUpdate:
// works [self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
// not works [self updateCellAtIndexPath:indexPath];
break;
default:
break;
}
}
But when I read the documentation for reloadRowsAtIndexPaths:withRowAnimation:, it says that I should not use this method if I want just to update values:
Call this method if you want to alert the user that the value of a
cell is changing. If, however, notifying the user is not
important—that is, you just want to change the value that a cell is
displaying—you can get the cell for a particular row and set its new
value.
Why then I can not update cell text label using just updateCellAtIndexPath?

Your updateCellAtIndexPath: method creates a new cell instead of updating the existing one.
That is OK if called from cellForRowAtIndexPath, but not when called from the FRC
delegate methods.
It should work if you implement and use the "standard"
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
which you can find e.g. when creating a new app using the (Master-Detail + Core Data)
template in Xcode.

Related

UITableView Data Inconsistency

I have a UITableView backed by a NSArray.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.data.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CellIdentifier";
UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];
}
id item = [self.data objectAtIndex:indexPath.row];
cell.item = item;
return cell;
}
Very standard. Now the problem is that reloadData will ask for numberOfSections and numberOfRows synchronously, but will call cellForRow asynchronously. So sometimes, by the time cellForRowAtIndexPath gets called, the data array has changed, and so [self.data objectAtIndex:indexPath.row] gets an out of bounds exception and crashes the app. How do I avoid this?
Note that every time I set the data array, I also call [self.tableView reloadData].
cellForRowAtIndexPath gets called frequently, (on scroll etc), you could just add a simple line of code to check if the size of the data array is smaller than the cell being requested. Although this means you might end up with blank cells.
I'd set breakpoints on both methods, right click the breakpoints -> "edit breakpoint" and tick "automatically continue after evaluating". Then click "add action" -> "debugger command" and then type "po data" or "po [data count]".
This will print information about the array in the debug console every time the breakpoint is hit (without stopping). You should then be able to look through the debug output and see where it is falling out of sync. Add some NSLog statements to to tell you which method is is being called etc and work from there.
I think the best way to avoid such a situation is to avoid user interaction while data is updated.May be you can show a screen to user that "updating.." and an activity indicator.
Another way is that to have another array to populate new data, handling can be done in separate thread and at times only it is assigned back to the datasource array with reloading call after that.There also a screen with same can be used while datasource array gets changed
Quick hack I used, try this and see if it works for you:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CellIdentifier";
UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];
}
// -----------------------------------------------------------------
// the magical line that prevents the table from fetching the data
// -----------------------------------------------------------------
if([indexPath row] < [self.data count])
{
id item = [self.data objectAtIndex:indexPath.row];
cell.item = item;
}
return cell;
}
:D
You should store a local array, that doesnt get modified. then, when your base array changes, you can update your storred array safely. Look into adding/removing cells from a table view using the built in api Add rows to existing UITableView section

pushing tableview results in cellForRowAtIndexPath exception

I am trying to build a multilayer xmlparser which shows data in a tableview and switches to the next view based on the selected row (most likely to the same viewcontroller).
I started with storyboard segues but as i am using dynamic cells i dont know how to create more than one push segue (because it needs to push to various viewcontrollers). So i kept the storyboard views, deleted all the segues and used the code below instead.
however it throws this exception when the new view tries to pupulate the row:
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'UITableView dataSource
must return a cell from tableView:cellForRowAtIndexPath:'
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"index: %i", indexPath.row);
switch (indexPath.row) {
case 0:
{
DetailViewController *detailViewController = [DetailViewController alloc];
[self.navigationController pushViewController:detailViewController animated:YES];
break;
}
//...
default:
{
LayerViewController *layerViewController= [LayerViewController alloc];
[layerViewController setStartUpWithIndex:indexPath.row andLayer:layercount];
[self.navigationController pushViewController:layerViewController animated:YES];
break;
}
}
}
setStartUpWithIndex:andLayer: is my init method...
the problem does not occur when im pushing via storyboard segue with the following segue code:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
[[segue destinationViewController] setStartUpWithIndex:indexPath.row andLayer:layercount];
}
i think i am missing something the "segue" method does which i need to include in my didSelectRowAtIndexPath as cellForRowAtIndexPath: works fine in the first layer of the view.
requested edit:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"LayerCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell.textLabel.text = [[articles objectAtIndex:indexPath.row]objectForKey:#"HerstellerName"];
// cell.textLabel.text = #"ololol";
return cell;
}
the articles array is working btw
You aren't creating any cells. I don't know why Apple didn't include this in the default implementation, but after:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
You need to add:
if (cell==nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
Otherwise you're just relying on dequeued cells which won't exist when the tableView loads.
Edit: And if you're not using ARC, you need to wrap that alloc method with an autorelease.
You are pushing another viewController to be viewed and this next viewController has no dataSource methods implemented. This what happened.
As far as I understood, you are trying to present an xml node with a table in the way that each table row consist child nodes of presented node. selecting row with chld node gets you to next table view which presents table with child nodes of selected node.
IMO you may need only one view controller to present current xml node, which changes only data to be presented in a table and reloads the table again. So no pushing view controllers is needed. You can use then method
– reloadRowsAtIndexPaths:withRowAnimation:
for all the nodes in the table, setting animation similar to changing view controller (aka sliding old table left)

Table View Not Properly Updating

My project is a large one, and I'm not sure which code snippet is causing the problem, so I'll just describe the problem. I have an initial view that is a UITableView. This view has a navigation bar, in which one of the buttons is an "edit" button. Pressing the "edit" button sends you into a modal view controller in which there is another UITableView. This UITableView has the attribute that it is a checklist table view (in which multiple items can be selected [checked] or deselected. Once you are finished choosing your items, it saves the array of chosen objects into an NSUserDefault. Now, you are back at the original page where the array of things you chose should be displayed on the UITableView. I change the array that is feeding the UITableView it's data to the array grabbed from the NSUserDefaults. I then call [tableView reloadData] and nothing changes. I would really appreciate any tips. If you guys think you know what part of the code is causing me grief, please respond and I'll post it. Thanks (and by the way, I know I should be making the main view controller the delegate of modal view controllers). Thanks in advance.
Call [[NSUserDefaults standardUserDefaults] synchronize]; after you make changes in the modal view controller. This will save the changes.
Be sure to update the cell content each time the table row is reloaded.
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Bleh *data = (Get your data for this row);
cell.textLabel.text = data.myValue;
cell.imageView.image = data.myImage;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
Try to reload your tableview on viewwillappear , when you back from modelviewcontroller method calls
- (void)viewWillAppear:(BOOL)animated{
[yourTableView reloadData];
}

Properties in custom UITableViewCell are not getting initialized for a UISearchDisplayController's table

I am using a UISearchDisplayController to be able to display a table with custom cells based on some data I am retrieving from a server.
First I set the UISearchDisplayController inside my UIViewController.
self.searchController = [[UISearchDisplayController alloc]
initWithSearchBar:self.mySearchBar contentsController:self];
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
My UIViewController also implements the UISearchBarDelegate, so I can determine when a search starts. I set up a block so when my api call returns it gets called and a dictionary of results is saved in the self.searchResults property:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// here we make the api call
[api getSomeInfo:searchBar.text complete:^(NSDictionary *json) {
self.searchResults = json;
[self.searchController.searchResultsTableView reloadData];
}];
}
Now, the problem I have is that in my UITableViewDataSource method, where I return the custom cell. My cell is instantiated, but it's IBOutlets never get initialized, so I cannot set their content (text, images, etc) properly:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (tableView == self.searchController.searchResultsTableView) {
cell = [tableView dequeueReusableCellWithIdentifier:#"SearchResultsCellIndentifier"];
if (cell == nil) {
cell = [[SearchResultsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
cell.customLabel.text = [self.searchResults objectForKey:#"customText"]; // cell exists but cell.customLabel is nil!!
}
}
Why is the content nil? Is there somewhere in my Custom Cell class where I should be setting the content up?
Thanks!
I think your problem is that you used the variable cellIdentifier when creating the cell, but a string constant when dequeuing.
Simply always recreating a cell will work, but is not efficient at all and leads to major memory leaks.
You should first set the cellIdentifier according to which table view you are in, and which kind of cell you need, then dequeue with that cellIdentifier, and then create a new one if needed.

Return NSIndexPath from custom cell ? (UITableView)

I need to get the NSIndexPath for a custom cell in a UITableView. Here's the problem: I send a NSNotification from my custom cell to my UITableViewController when editingDidBegin gets called from a UITextField in my custom cell. In my UITableViewController, I resize the UITableView when the UITextField began editing, and then want the table view to scroll to the Cell in which the UITextField is first responder. But I can't figure out how to return the indexPath of the cell where the UITextField is being edited. I have tried so many ways, but its still not working. One way is this: in my cstomCell class i select the row using [self setSelected:YES] and in my TV controller then if I NSLog the row of [self.tableV indexPathForSelectedRow] it always returns 0 even though its always not 0.
Just give the cell a value for its tag property. Then you can get that cell by calling this
UITableViewCell *cell = (UITableViewCell *)[self.tableView viewWithTag:tagValue];
then once you have the cell you can get the NSIndexPath like this
NSIndexPath *indexPath = [self.tableView indexPathForCell:nextResponderCell];
Since you have a UITextField in your custom cell you can place cell.textField.delegate = self; in the
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
data source method. That way you will not have to setup a NSNotification in the Custom Cell. Also in this same method you can tag both your cells text field like this cell.textField.tag = indexPath.row; and the cell like this cell.tag = indexPath.row;
Now that you have set the UITextField delegate, you can now place this method in your UITableViewController class
- (void)textFieldDidBeginEditing:(UITextField *)textField {
CustomCell *cell = (CustomCell *)[self.tableView viewWithTag:textField.tag];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
}
The above UITextField delegate method should get you the indexPath for the cell you have currently selected.
try this code..
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
UITableView *tableView;
UITableViewCell* superViewCell = [self returnSuperCellViewOfTextField:textField];
if(superViewCell)[tableView scrollToRowAtIndexPath:[tableView indexPathForCell:superViewCell] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
-(id)returnSuperCellViewOfTextField:(id)viewToCheck
{
id superView = [viewToCheck superview];
if([viewToCheck isKindOfClass:[UITableViewCell class]] && superView)return superView;
else if(([viewToCheck respondsToSelector:#selector(superview)] && superView)) return [self returnSuperCellViewOfTextField:superView];
return nil;
}
Does this (your answer that you gave before below)still works if you have multiple texfields in each cell?:
UITableViewCell *cell = (UITableViewCell *)[self.tableView viewWithTag:tagValue];
then once you have the cell you can get the NSIndexPath like this
NSIndexPath *indexPath = [self.tableView indexPathForCell:nextResponderCell];
Since you have a UITextField in your custom cell you can place cell.textField.delegate = self; in the
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
data source method. Also in this same method you can tag both your cells text field like this cell.textField.tag = indexPath.row; and the cell like this cell.tag = indexPath.row;
Now that you have set the UITextField delegate, you can now place this method in your UITableViewController class
- (void)textFieldDidBeginEditing:(UITextField *)textField {
CustomCell *cell = (CustomCell *)[self.tableView viewWithTag:textField.tag];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
I have a tableview with 8 cells, and in each cell i have 6 texfields, and also the tableview is dynamic, so the user can insert more cells...but i´m having trouble in how to save the text the user enters in each texfield. Is it possible that with "cell.textField.tag = indexPath.row", the controller identify´s the texfield i´m in?
I like to tag my cells like so:
/* Create a tag based on the section and row of a cell(used to retrive the cell for the beginEditingNextCell method. */
-(NSInteger)tagForIndexPath:(NSIndexPath*)path {
return path.section * 1000 + path.row + 1;
}
The other way is to pass the beginEditing data using a custom delegate, and include a cell model, which you can then use to figure out your path depending on how you are storing data.
Check out my github project, it has the code to handle this case, as well as resizing for the keyboard and tabbing to the next editable cell: https://github.com/andrewzimmer906/XCell