I have a dynamic populated list. I am trying to have it return nothing if a variable is a certain value.
Here is what I am doing:
/* FIRST VALIDATE TIME LEFT TO MAKE SURE IT STILL EXIST */
NSString *check = [self.googlePlacesArrayFromAFNetworking[indexPath.row] objectForKey:#"time_left"];
if(![check isEqualToString:#"expired"])
{
return cell;
}
else
{
return NULL;
}
Now if expired exist than it returns NULL but that does not work. It crashes with the following error:
'NSInternalInconsistencyException', reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
Not sure how I can fix this, suggestions and thoughts?
David
UPDATE:
cell:
static NSString *CellIdentifier = #"timelineCell";
FBGTimelineCell *cell = (FBGTimelineCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell = [[FBGTimelineCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
[cell initTimelineCell];
Your data source must return consistent values across all of its methods. The number of rows in your tableView is controlled by the return value from numberOfRowsInSection and cellForRowAtIndexPath must return a valid cell for each row.
Validation and modification of the data in the data source must be performed outside of cellForRowAtIndexPath and [tableView reloadData] or [tableView deleteRowsAtIndexPaths] called to update the table display
Update
It is hard to provide an example without understanding what is driving the "expired" behaviour - Is this data that is retrieved from the web service or is it the result of information ageing after it is retrieved. If the former then I would suggest that you transfer the data from the web service result array into an array that drives the tableview and filter the expired data while you are copying it. If you need to periodically scan for expired data then you can use something like the following -
You would need to have something, such as an NSTimer trigger this method periodically, or if you are re-fetching data from the network, that could be the trigger to run this method -
-(void)expireData
{
for (int i=0;i<self.googlePlacesArrayFromAFNetworking.count;++i) {
NSDictionary *dict=[self.googlePlacesArrayFromAFNetworking objectAtIndex:i];
if ([[dict objectForKey:#"time_left"] isEqualToString:#"expired"])
{
[self.googlePlacesArrayFromAFNetworking removeObjectAtIndex:i];
[self.tableView deleteRowsAtIndexPaths:#[[NSIndexPath indexPathForItem:i inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
--i; // Array is now one element smaller;
}
}
}
Note that this method modifies the self.googlePlacesArrayFromAFNetworking array. If this is unacceptable then you need to copy this array to another array to drive the UITableView.
Another approach is to scan the array in numberOfRowsInSection and work out how many non-expired elements there are and return that. Then in cellForRowAtIndexPath you need to scan forward through the array to find the next non-expired element; This is pretty messy though because your indexPath rows and your array indices will be out of sync.
It depends on what you want. There are two options:
If you want your table view to have empty rows for the data that are expired, you need to configure and return a blank cell.
If you want those cells to be missing, you need to return the correct number of rows for -tableView:numberOfRowsInSection (that is, subtract out the number of expired rows in your calculations before returning from that method). Then, your data source will never be asked for a cell for that index path.
Update: by the way, you should return nil, not NULL, when the parameter is expecting an Objective-C object. nil is equivalent to (id)0, whereas NULL is equivalent to (void *)0. [source]
You always have to return a cell. What you need is not return nil but just don't populate it.
Related
I have a custom TableViewCell that I am getting data from a database in an asynchronous function that returns a UserObject that has the data I need in it. My problem is that the cellForRowAtIndexPath is returning the cell before that Asynchronous block is completed. How do i solve this problem?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"ImageWorkoutCellCollapsed";
ImageWorkoutCellCollapsed *cell = (ImageWorkoutCellCollapsed *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if(cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"ImageWorkoutCellCollapsed" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
WorkoutObject *workout = [[WorkoutObject alloc]init];
workout = [appDelegate.workoutObjectsArray objectAtIndex:indexPath.row];
cell.workoutTitle.text = workout.workoutTitle;
cell.workoutViewCount.text = workout.workoutDescription;
__block UserObject *cellUserObject = [[UserObject alloc]init];
[dbPointer getUserObjectFromDB:workout.workoutOwnerID completion:^(UserObject *result)
{
cellUserObject = result;
}];
cell.userName.text = cellUserObject.username;
return cell;
}
You should turn the cell update into a reload of the row as follows:
1) You should use dequeueReusableCellWithIdentifier:forIndexPath rather than the version you are using.
2) You should not need to check (cell == nil) as 1) should never return nil.
3) Add a new NSMutableDictionary property to cache UserObject's by workoutOwnerID.
4) When you are asked for a cell, lookup the dictionary from 3) to see if it has your data object. if not, then run the DB query. If it has the object, set the cell values.
5) In your completion handler for the DB lookup, cache the returned object into the new dictionary by workoutOwnerID. Then simply request the table to reload the row.
The result is that the cells are updated when the data they represent is updated.
You should make sure cellForRowAtIndexPath is not being called before the data is available.
You have a count of the number of rows, you must be setting this count to N but you haven't yet fetched N data items.
Don't set this number until you have fetched all the data.
OR
As data arrives continually update the number or rows and refresh the table view.
OR
Return the cell with placeholder data. Then once the actual data for the cell is available update the content and refresh the table.
OR
All of the above solutions involves moving the data fetch out of cellForRowAtIndexPath. However IFF the call to fetch the data is quick and thus won't slow down the drawing of the table, you need to convert the fetch from being asynchronous to synchronous. But it is not good design for a view controller component to directly access a db, instead it should be going to a model and the model should abstract away the implementation detail that the data is in a database.
I am currently retrieving objects from Parse.com and saving them to CoreData.
I am then next using NSFetchedResultsController to retrieve objects from CoreData. These objects will then be used to create a table view. Everything i retrieve from CoreData is stored in an NSArray using the following code:
NSArray *fetchedObjects = _fetchedResultsController.fetchedObjects;
Using the fetched objects array i am wanting to load a specific nib file depending on the type of each object. So using the following for loop within cellForRowAtIndexPath i am trying to achieve this:
for (NSManagedObject *o in fetchedObjects)
{
if ([[o valueForKey:#"type"] isEqual: #"Type1"])
{
Type1CustomCell *cell = (Type1CustomCell *)[tableView dequeueReusableCellWithIdentifier:#"type1CustomCell"];
return cell;
}
else if ([[o valueForKey:#"type"] isEqual: #"Type2"])
{
Type2CustomCell *cell = (Type2CustomCell *)[tableView dequeueReusableCellWithIdentifier:#"type2CustomCell"];
return cell;
}
}
The previous code is just an example using 2 types, but within the app there may be more.
The return statement cause the loop to end, which means the loop never gets past the first object. Could someone please give me a point in the right direction of how to load multiple nib files depending on the type of the object I have retrieved?
Thanks
So, the only time you dequeue and return reusable collection view cells is in the datasource method that asks for a cell.
When this method fires, it's given you a specific index path--the index path for the row it's trying to create.
You don't need to be looping through anything in this method. You just need to go to the right index of whatever collection you're storing your data in, grab the object at that index. Use that data to determine what cell to return.
Instead of a forin loop, just grab a single object.
NSManagedObject *obj = [fetchedObjects objectAtIndex:indexPath.row];
if ([[obj valueForKey:#"type"] isEqual: #"Type1"]) {
// etc...
You'll still need a large if-else structure here, I believe, but now we're just checking an object at the specific index the table view is trying to create the cell for.
I have been struggling with this for a week and my head is about to explode.
Basically I use Prototype Cell, in CellWillAppear I did a little customizations like background color. Nothing fancy.
Due to this, my table view is always empty at start up (no cell) unless the array (data source) is filled with something. So what I did was in NumberOfRowsInSection:
return dataArray.count < 10? 10 : dataArray.count
I am doing this because I would like to see at least some empty cells when there is no data.
Meaning it will show on start up at least 10 empty cells.
To add data to the cell, I call the delegate method in my tableviewcontroller each and every time to add one single entity in the data array (am doing this, because I think it would be faster than waiting until the whole array is filled then call [self.tableView reloadData];) and then refresh it by using reloadRowsAtIndexPaths. But it crashed every single time when it reached to index 10 (error: ... before update number of data was 10, but after update is 11).
What I really want is:
1.) prepare some data
2.) send it to uitableview controller and add it to an array there, instead of waiting and then sending a whole array to table view and refresh at once.
3.) reload just one row after the update (instead of using reloadData -> since I have different color of cell, the whole reload thing cause my table view flash madly).
The one thing I am doing to cell customization is in willDisplayCell:
What I did there is to change the background color of the cell. Again, nothing fancy.
But since there is no data at start up, no cell is ever visible (ui tablew with no cell at displayed at all), unless I did this
return dataArray.count < 10? 10 : dataArray.count;
just so there are at least 10 empty cells showing (WHY do I have to do the above just to display some customized empty cells beats me...).
Using reloadData is to refresh no problem, but since I am updating the data source array in table view every time data is ready instead of saving all prepared data to this array and send it over to table view to update by using reloadData, I would like to update row by row.
I kind of feel that the error comes from the fact that, if I add one item in the array and then call reloadRowsAtIndexPath, it will say "Ok, you had one item before, but after update there is 2! Inconsistency.."
I have already tried using [tableView beginUpdate]; and [tableView endUpdate];
Nothing has worked so far.....
So to sum up: how can I have different colors of cells showing even when the data array is empty on start up (just like the default ui table view with cells displaying completely even with no data) and update just one of the cells once a piece of data is ready instead of updating the whole ui table view with reloadData?
Many thanks in advance, please advise. Regards.
"how can I have different colors of cells showing even when the data array is empty"
Don't have an empty array, have a mutable array where all the members are initially empty strings, and replace those with your real data when you get it.
"update just one of the cells once a piece of data is ready"
Update your array with the new data, and then use reloadRowsAtIndexPaths:withRowAnimation: to update the table. If you want to see the table update row by row (slow enough to see), then put your data in a temporary array first, then add it one element at a time using performSelector:withObject:afterDelay:, calling reloadRowsAtIndexPaths:withRowAnimation: after each addition.
It's a little hard to tell exactly what you want, but here is an example of what I mean. This table displays 20 empty rows, all with different colors, for 2 seconds, then it replaces the empty strings in displayData with the strings in theData one by one at a rate of 10 per second.
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSMutableArray *displayData;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.displayData = [#[#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#""] mutableCopy];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine",#"ten",#"Black",#"Brown",#"Red",#"Orange",#"Yellow",#"Green",#"Blue",#"Violet",#"Gray",#"White"];
[self.tableView reloadData];
[self performSelector:#selector(addData) withObject:nil afterDelay:2];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.displayData.count;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UIColor *cellTint = [UIColor colorWithHue:indexPath.row * .05 saturation:1.0 brightness:1.0 alpha:1.0];
cell.backgroundColor = cellTint;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.displayData[indexPath.row];
return cell;
}
-(void)addData {
static int i = 0;
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
if (i < self.displayData.count) [self performSelector:#selector(addData) withObject:nil afterDelay:.1];
}
If you don't want any delay between row updates, and you want to make it work when displayArray has a different number of rows that theData, this version of addData should work:
-(void)addData {
static int i = 0;
if (i < self.displayData.count && i< self.theData.count) {
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}else if (i >= self.displayData.count && i< self.theData.count) {
[self.displayData addObject:self.theData[i]];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}
}
I have a UITableView with two sections. Based on user interactions (selections and deselections), my datasource and UITableView are updated to move data between sections. Initially their is only data in section 0. When I tap a cell, willSelectCellAtIndexPath and didSelectCellAtIndexPath get called. As expected, when I tap the same cell again, didDeselectCellAtIndexPath is called.
Even after I begin to move data down to section 1 and select and deselect, the UITableView's delegate methods are called appropriately.
Once all data has been moved to Section 1, the UITableView begins to exhibit strange behavior. I can initially select a call and didSelectCellAtIndexPath is called. However, when I tap it again, didDeselectCellAtIndexPath is never called. Instead, any taps on the selected cell (I have confirmed it is indeed selected through [tableView indexPathsForSelectedCells] or any other cells in Section 1 only result in willSelectIndexPath and didSelectIndexPath getting called.
I have quite a bit of code in these delegate methods which is unrelated (I believe).... I do not explicitly change the selected state of a cell anywhere. I have posted willSelect method and can post more if necessary.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (remainingItemIsSelected && indexPath.section == 0) {
//other cells in the remaining items section are selected and a cell from that section is being selected
NSMutableIndexSet *arrayIndexesToBeDeleted = [[NSMutableIndexSet alloc] init];
for (NSIndexPath *previouslySelectedIndexPath in [tableView indexPathsForSelectedRows]) {
if (((ReceiptItem *)[self.remainingReceiptItems objectAtIndex:previouslySelectedIndexPath.row]).allocated == YES) {
//update data sources
NSLog(#"%#%i%#,%i",#"Section #:",previouslySelectedIndexPath.section,#" Row #:",previouslySelectedIndexPath.row);
[self.assignedReceiptItems addObject:[self.remainingReceiptItems objectAtIndex:previouslySelectedIndexPath.row]];
[arrayIndexesToBeDeleted addIndex:previouslySelectedIndexPath.row];
//update index path arrays
[self.receiptItemsToDeleteIndexPaths addObject:previouslySelectedIndexPath];
[self.receiptItemsToAddIndexPaths addObject:[NSIndexPath indexPathForRow:self.assignedReceiptItems.count-1 inSection:1]];
//update the pressed indexpath to equal to resulting indexpath to pass on to the didSelect method
if (previouslySelectedIndexPath.row < indexPath.row) {
indexPath = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:0];
}
}
}
//Delete assigned items from the remaining receipt items
[self.remainingReceiptItems removeObjectsAtIndexes:arrayIndexesToBeDeleted];
//update table (move allocated item down)
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:self.receiptItemsToDeleteIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:self.receiptItemsToAddIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
if (self.remainingReceiptItems.count == 0) {
[tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
}
//other cells in the remaining items section are selected and a cell from assigned items is being selected
return nil;
}
return indexPath;
}
From the Documentation for UITableViewDelegate:
tableView:willDeselectRowAtIndexPath:
This method is only called if there is an existing selection when the user tries to select a different row. The delegate is sent this method for the previously selected row.
If you think this through you will find that what you encounter is expected behavior. Tapping a row that is selected does not call will/didDeselctRowAtIndexPath on this row.
Instead, you could handle this in didSelectRowAtIndexPath for the selected row, i.e. deselect it there.
Possible Alternative
That being said, I think you are abusing the UITableView class. It is really not designed to do this moving stuff. You have no doubt noticed yourself that you have to write a lot of code to make this work -- the very reason you are encountering intractable errors.
It seems to me that a much cleaner (and ultimately more flexible) solution would be to have two separate table (or other) views that notify each other via delegates about datasource changes. Maybe a bit more work setting it up, but surely much less trouble down the road.
I am having some trouble with UITableView's reloadData method. I have found that it only calls cellForRowAtIndexPath if there are new cells being added or taken away.
Example: I have five cells, each containing five strings. If I add a new cell and call reloadData, the table is updated and I see it. Yet if I go into one of the five cells, add a new string, then return and call reloadData, none of the table view's delegate methods is called.
My question: Is it possible to force the table view to completely reload the data in all of its visible cells?
I found the problem- I had my cell customization code in the if(cell == nil) block, so because the cells were being recycled, they weren't being changed. Taking my customization code out of that block fixed the problem.
I've found that reloading sections reloads data more readily. If you just have one section you can try:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
You can try this
[tableView reloadData];
or
[tableView deleteRowsAtIndexPaths:[NSMutableArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
put this in commitEditingStyle method.
Well, the table view's only going to call cellForRowAtIndexPath on the visible cells but if you do call reloadData it will do that so there's something else going on here and I'm not sure what it is.
Even if your tableview has 50 rows, there only will exist as much cells as can be visible at one time. That's the whole story behind the reuseIdentifier. So forcing 'all the cells' doesn't exist. If a new cell appears, the data is loaded dynamically.
The only way to change a cell is to change the data that is delivered by the dataSource method cellForRowAtIndexPath.
Do not take your code out of the if (cell == nil) block. Instead, create a representative identifier for the cell you're making; try and make sure that all of the cell's content is referred to in the identifier. For example, if you have 3 numbers showing, make sure to have those three numbers in the identifier in a unique way that would only refer to a cell that has such content.
Let's say you have three NSArray properties in your class, array1, array2, and array3 that have int values wrapped inside of NSNumber objects. You want to use those NSArrays to fill a UITableView, this is what I'd do:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = [NSString stringWithFormat:#"%#-%#-%#",
[[array1 objectAtIndex:indexPath.row] intValue],
[[array2 objectAtIndex:indexPath.row] intValue],
[[array3 objectAtIndex:indexPath.row] intValue]];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:identifier] autorelease];
//Build your cell here.
}
return cell;
}