TableViewCell subtitle won't change unless the cell is tapped - ios

The cells in my table have a subtitle set that will show some extra information loaded from a web server. When the app loads the subtitle will just say "Loading..." and then when the response is received, and parsed the cell is updated.
The problem is, unless I tap on the cell the subtitle will stay at "Loading...". As soon as I tap on it it updates to the correct subtitle.
Here I initialize the cell, and set the temporary subtitle while the http request is performed
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// Setting the tableviewcell titles
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.detailTextLabel.text = #"Loading...";
cell.detailTextLabel.enabled = NO;
return cell;
}
I've tried making calling the request method in different places:
willDisplayCell and in cellForRowAtIndexPath.
The method that gets the data from the web server uses an asynchronous NSURLConnection which when a successful response is received I update the cell subtitle text using:
// Map Reduce the array used by the TableView
for (int i = 0; i < [self.routes count]; i++) {
if(cellMatches){
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
cell.detailTextLabel.text = #"Data received!";
cell.userInteractionEnabled = YES;
cell.textLabel.enabled = YES;
cell.detailTextLabel.enabled = YES;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
I know that you can reload a specific cell using tableView reloadRowsAtIndexPaths but that doesn't seem to work when I implement this code:
[self.tableView beginUpdates];
// Change cell subtitle
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
I have a timer set up to call the request method every 30 seconds, when that is called it works no problem and updates the subtitle right away without me having to tap it. So I think the problem is that the cell isn't initialized or maybe it's being reinitialized after the web request is made. But I don't call reloadAllData during this method.

What you need to do is update your cellForRowAtIndexPath so it checks for the data. If the data is available, set the subtitle to the data, otherwise show "Loading". This requires that you have some sort of data model that stores the data when it is received.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
BOOL enabled = YES;
NSString *subtitle = ... // get the value from the data model
if (subtitle) {
cell.textLabel.text = ... // whatever value goes here
cell.detailTextLabel.text = subtitle;
cell.detailTextLabel.enabled = YES;
} else {
// There's no value for this row yet - show "Loading"
cell.textLabel.text = ... // whatever value goes here when loading
cell.detailTextLabel.text = #"Loading";
cell.detailTextLabel.enabled = NO;
}
return cell;
}
Be sure you set the same set of cell properties in both halves of the if/else statement as needed.
When you get new data and update your data model, simply call reloadRowsAtIndexPaths: for the proper cell path. The above code will then properly update the cell.
The code you have now to update a cell should be removed since it is not the proper way.

Try this:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
Instead of:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];

Alongside with rmaddy's solution, I also needed to add one important thing that I found in a similar question:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
Fixed the issue completely.

Related

Selecting different UITableViewCells from indexPath alone

I have a scenario where upon selecting a UITableViewCell in didSelectRowAtIndexPath:, I need to load and get the information from a different UITableViewCell.
I'm registering and using two different xibs to be used as my tableViewCells to allow for some more customization.
- (void)viewDidLoad{
[super viewDidLoad];
self.TABLE_ROW_HEIGHT = 66;
self.tblView.delegate = self;
self.tblView.dataSource = self;
[self.tblView registerNib:[UINib nibWithNibName:#"BasicCell" bundle:nil] forCellReuseIdentifier:#"BasicCell"];
[self.tblView registerNib:[UINib nibWithNibName:#"DetailCell" bundle:nil] forCellReuseIdentifier:#"DetailCell"];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
//Property of the view controller which is an IndexPath
self.selectedIndex = indexPath;
BasicModel *basicModel = [self.models objectAtIndex:indexPath.row];
[self.apiClient detailModelSearch:basicModel.id];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if([self.selectedIndex isEqual:indexPath]){
return 400.0f;
}
return 66.0f;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
BasicModel *basicModel = [self.models objectAtIndex:indexPath.row];
UITableViewCell *tableCell = nil;
if([self.selectedIndex isEqual:indexPath]){
DetailCell *detailCell = [tableView dequeueReusableCellWithIdentifier:#"DetailCell"];
tableCell = detailCell;
}
else{
BasicCell *basicCell = [tableView dequeueReusableCellWithIdentifier:#"BasicCell"];
tableCell = basicCell;
}
return tableCell;
}
-(APIClient *)apiClient{
if(!_apiClient){
_apiClient = [APIClient new];
__weak ViewController *_self = self;
_apiClient.detailModelSearchFinished = ^(DetailModel *detailModel){
_self.detailModel = detailModel;
//Problem is here
DetailCell *cell = [_self.tblView cellForRowAtIndexPath:_self.selectedIndexPath;
dispatch_async(dispatch_get_main_queue(), ^{
[_self.tblView beginUpdates];
[_self.tblView endUpdates];
[_self.tblView reloadData];
});
};
}
return _apiClient;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.models.count;
}
The basic structure is as follows.
App load and loads all BasicModels into the the models array.
User selects a cell which prompts an API detail search request
When detail search request is finished, the callback returns a DetailModel
What should happen next is since I know the selected index path of the touched cell, I want to use the DetailCell instead of the BasicCell to present the detailedInformation that comes from the DetailModel. My problem is when I call
DetailCell *cell = [_self.tblView cellForRowAtIndexPath:_self.selectedIndexPath;
I always receive the BasicCell that does not have the detailed view components I need to bind the detailModel to.
BasicCell xib
Detail Cell Xib
Table View Normal:
Table View Expanded with detail Cel xib
Ok now is very clear.
I can think of two ways, one is if you don't care about fancy animations just remove the (basic) cell and insert a new (detail) cell at the same index path, only after tho you have updated the model as well and perform the eventual type checks.
Quick and dirty, if you want something more clean you may want to refactor the model objects using polimorfism or other suitable patterns.
Another way is to update directly the cell with the received data. You may apply some fancy animations but loosing possibly some performances advantages.
Pretty simple solution actually. Reload data must be called before I can grab the expanded cell. Simple as this:
[_self.tblView beginUpdates];
[_self.tblView endUpdates];
[_self.tblView reloadData];
DetailCell *expandedCell = (DetailCell *) [_self.tblView cellForRowAtIndexPath:_self.selectedIndex];
expandedCell.lblData.text = #"IT WORKS!";
});

PFQueryTableViewController jump on fetch

Edit 1
To be clear, [self loadObjects] is not my method it is a method on the PFQueryTableViewController class supplied by parse to pull in new data.
I suspect this might be being caused by the table drawing code, as the tablecellview is configured to be auto-adjust it's height.
Here is the table drawing code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
object:(PFObject *)object
{
//Setup estimated heights and auto row height
self.tableView.estimatedRowHeight = 68.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
//Give the cell a static identifier
static NSString *cellIdentifier;
socialPost* post = object;
//Check to see what sort of cell we should be creating, text, image or video
if (object[#"hasImage"] != nil) {
cellIdentifier = #"posts_with_image";
} else {
cellIdentifier = #"posts_no_image";
}
//Create cell if needed
hashtagLiveCellView *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[hashtagLiveCellView alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:cellIdentifier];
}
// Configure the cell to show our imformation, loading video and images if needed
cell.postTitle.text = [NSString stringWithFormat:#"#%#",object[#"userName"]];
cell.postText.text = [NSString stringWithFormat:#"%#",
object[#"text"]];
[cell.userImage setImageWithURL:[NSURL URLWithString:post.userImageURL]];
[cell.postImage setImageWithURL:[NSURL URLWithString:post.imageURL]];
//Set ID's on the custom buttons so we know what object to work with when a button is pressed
cell.approveButtonOutlet.stringID = object.objectId;
[cell.approveButtonOutlet addTarget:self action:#selector(approvePostCallback:) forControlEvents:UIControlEventTouchUpInside];
cell.deletButtonOutlet.stringID = object.objectId;
[cell.deletButtonOutlet addTarget:self action:#selector(deletePostCallback:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
Original
I have a PFQueryTableViewController that i am loading with object from parse.
I have a scheduled task set to run every 20 seconds that calls:
[self loadObjects]
To fetch any new objects, or any changed to objects that have happened.
That all works fine, however if i am scrolled halfway down the tableview when the loadObjects is called the page jumps back to the top. Even if there are no new or changed data available.
Is there an easy way around this, before i start looking into hacky ways to catch the reload and force the table to stay where it is.
Thanks
Gareth
When you're calling loadObjects you load the objects from start. And there for you get the first results again.
Try to change [self loadObjects]; to [self.tableView reloadData];.

Add new UITableView row with custom text

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

UITableViewCell loses highlighted selection

I am using [tableview reloadData]; to reload the data in my UItableView, however when I use this I loose my highlight on my UItableVewCell.
I would like to know the best way to reinstate this highlight.
I set a tempIndexPath when the user selects the cell they edit the information then I call reloadData, then inside cellForRowAtIndexPath I use this code to re-highlight the cell however its not working.
if ([tempIndexPath isEqual:indexPath]) {
cell.selected = YES;
}
This code keeps the highlighted selection, and is safe with resorting/inserts/repositioning since it keeps a reference to the underlying object from the model, instead of the index path. It also scrolls to the selection, which is helpful when the updated model causes the selection to be moved out of the current scroll position's frame.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//Save the selected object at this row for maintaining the highlight later in reloadData
_selectedItem = [_items objectAtIndex:indexPath.row];
}
- (void) reloadData
{
[_itemsTable reloadData];
//Reselect and scroll to selection
int numItems = _items.count;
for (int i = 0; i < numItems; i++) {
NSDictionary *dict = [_numItems objectAtIndex:i];
if (dict == _selectedItem) {
//This is more reliable than calling the indexPathForSelectedRow on the UITableView,
// since the selection is cleared after calling reloadData
NSIndexPath *selectedIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[_itemsTable scrollToRowAtIndexPath:selectedIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
[_itemsTable selectRowAtIndexPath:selectedIndexPath animated:FALSE scrollPosition:UITableViewScrollPositionNone];
break;
}
}
}
Save the selected row, reload your table view's data and select the row again.
NSIndexPath* selectedIndexPath = [tableView indexPathForSelectedRow];
[tableView reloadData];
[tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
You should know that this is dangerous, because if new items were added to the table view before the selected row, the selection will not be the correct cell. You should calculate how many rows were inserted before the row and adjust the selectedIndexPath accordingly.

How to reload UITableView without stopping cell selection animation

When the users taps on a cell, I want to update my UITableView; including the contents of this tapped cell. Easiest way is to update internal parameters and then invoke [self.tableView reloadData];.
However, reloadData immediately stops the nice blue->none selection animation of my tapped cell.
Is there a (standard) way to update my table cells without stopping the tapped cell's animation?
Note in this case I don't add or delete cells; I just want to contents to change (e.g. start an activity indicator, or change the color of labels.)
In your case you can just get pointers to all visible cells and update them. Something like this:
- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
NSArray* visibleCells = [tableView indexPathsForVisibleRows];
for (NSIndexPath* indexPath in visibleCells)
{
UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self updateCell:cell atIndexPath:indexPath]; // Your method, which updates content...
}
}
And if you want to update other cells content you can use something like this:
- (void)tableView:(UITableView*)tableView willDisplayCell:(UITableViewCell*)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
[self updateCell:cell atIndexPath:indexPath]; // Your method, which updates content...
}
So your cells will always display correct content.
About creating content:
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* CellIdentifier = #"Cell";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
[self createContentForCell:cell atIndexPath:indexPath]; // so here to create content or customize cell
}
return cell;
}
Or maybe you could simply delay the reload of your table data as described for example here: Delay reloadData on UITableView

Resources