Asynchronous TableViewCell Data - ios

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.

Related

Returning nill cell based on data

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.

Getting URL from cell using something like indexPath.row

I am trying to get a URL from a cell. To do this, I am using NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; and then would like to do something like NSURL *url = self.finalURL[indexPath.row] but because indexPath.row is only for Arrays, this doesn't work. Is there a way to achieve the same thing as indexPath.row but for objects not in an array.
Here is how I am saving the url:
cell.finalURL = self.finalURL;
A cell doesn't have a URL, unless you create a subclass of the cell and add that property to is. Conventionally, you will have an array of objects, strings, dictionaries, etc., and that is your tableView's data source.
If I had an array with three NSURLs in it called myArray that contained google, amazon, and bing, and I wanted to display three cells with the respective labels matching the items in the array, I would implement the following code:
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// we only want a single section for this example
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// this tells the tableView that you will have as many cells as you have items in the myArray array
return myArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// first we try to reuse a cell (if you don't understand this google it, there's a million resources)
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
// if we were unable to reuse a cell
if (cell == nil) {
// we want to create one
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
// here is where we do ANY code that is generic to every cell, such as setting the font,
// text color, etc...
cell.textLabel.textColor = [UIColor redColor];
}
// here is where we do ANY code that is unique to each cell - traits that are based on your data source
// that you want to be different for each cell
// first get the URL object associated with this row
NSURL *URL = myArray[indexPath.row];
// then set the text label's text to the string value of the URL
cell.textLabel.text = [URL absoluteString];
// now return this freshly customized cell
return cell;
}
That, along with the rest of the default tableview code and setting up the array itself, results in the following:
When a user taps on a cell you can access the URL in the array and do something with it like so:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// first deselect the row so it doesn't stay highlighted
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// get the URL associated with this cell by using indexPath.row as an index on your
// data source array, if you tapped the first cell, it will give you back the first URL in your array...
NSURL *selectedURL = myArray[indexPath.row];
// do something with that URL here...
}
Think of your table view's data source as a bunch of little cubbies. You can create the data source in a million different ways, but once you have it you basically take the items and place them in numbered cubbies. Your table view create's itself based on what's in those cubbies, so to make the first cell it looks in the first cubbie, and so on, and later on when a user selects a cell from that tableview, all the table view does is tell you the cubbie number that was selected, and it's your job to use that information to retrieve the data from that specific cubbie and do what you need to with it. Hope that helps!

Trying to load only certain entities into a table view

I have a data model with entity Customer. The Customer has attributes like name, address...etc. One of these attributes is a call back date. I want to load into the table only the Customers with call back date of today. below is the code I have to check to see if the dates are equal and then to create the cell. The problem is when the dates are not equal and it skips the creation of the cell. How do I skip that specific customer and move to the next one?
if(date==date2 && month==month2 && year==year2)
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSString *string = [NSString stringWithFormat:#"%# %#", cust.firstName, cust.lastName];
cell.textLabel.text = string;
return cell;
}
return nil;
}
I'd take a different route altogether.
Rather than not showing anything in the cell, simply don't provided data for there to even be a cell. You mention that you're using models so I assume you're using core data?
If so, then change your predicate when you fetch your models to ignore all objects that don't meet your criteria. Then you can just show every object in your table as you know that there aren't any you don't want.
Alternatively, fetch everything (if perhaps you're not using core data) then apply a predicate to the array of data you're using and filter it out that way.

UITableView/UITableView Cell Index Issue

I am making a bookmarks page for my web browser and the problem is that everytime I add a new object, I have to set the properties since I created a custom cell (I must set the text of two labels) and so I need a way to only edit the newly added object...I'm familiar with indexes but not able to come up with any solutions to this problem...For example when I bookmark the first page its fine, but once I bookmark 2 pages the 2 cells are the exact same...Any Ideas?
Heres My Code:
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"CustomCell";
CustomCell *cell = (CustomCell*)
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:nil options:nil];
cell = (CustomCell *) [nib objectAtIndex:0];
}
// Set up the cell...
NSString *theTitle=[webView stringByEvaluatingJavaScriptFromString:#"document.title"];
NSString *currentURL = webView.request.URL.absoluteString;
cell.websiteTitle.text = theTitle;
cell.websiteURL.text = currentURL;
universalURL = currentURL;
return cell;
}
When setting up the cell I need to point to the newest cell!
Thank You In Advance!
Your approach cannot work. You cannot use the cells as the store for the titles and URLs, because cells are reused (and because it is bad design). A table view allocates only cells for the visible rows and reuses a cell for a different row when you scroll the table view.
Instead you should store the titles and URLs in a separated data source, for example an NSMutableArray *bookmarks where each item in the array is a NSDictionary with "title" and "URL" keys.
To add a bookmark to your table, you just append a new entry to the array and call reloadData on the table view.
The tableView:cellForRowAtIndexPath: method can then use the bookmarks array with the row number indexPath.row to fill all elements of the cell.

updating a UI table view cell with upload status - iOS

Hell everyone :)
My experience with the UITablewView Controller in iOS is unfortunately quite limited. What I need in my application is a UI table view which contains one custom cell for each active upload currently being uploaded to a webserver (videos, audio, etc).
Each of these uploads run asynchrounously in the background, and should all be able to update things such as UILabels in their respective cells saying something about the update progress in percentage, etc.
Now I have found a solution which works. The problem is I do not know if it is actually secure or not. Based on my own conclusion I don't really think that it is. What I do is simply to retrieve a reference of the UIViews from a cell which is getting created, and then store those references in the upload objects, so they can change label text and so on themselves.
My Own Solution
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CustomCellIdentifier = #"CustomCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"UploadCellView" owner:self options:nil];
if ([nib count] > 0)
{
cell = customCell;
}
else
{
NSLog(#"Failed to load CustomCell nib file!");
}
}
NSUInteger row = [indexPath row];
UploadActivity *tempActivity = [[[ApplicationActivities getSharedActivities] getActiveUploads] objectAtIndex:row];
UILabel *cellTitleLabel = (UILabel*)[cell viewWithTag:titleTag];
cellTitleLabel.text = tempActivity.title;
UIProgressView *progressbar = (UIProgressView*)[cell viewWithTag:progressBarTag];
[progressbar setProgress:(tempActivity.percentageDone / 100) animated:YES];
UILabel *cellStatusLabel = (UILabel*)[cell viewWithTag:percentageTag];
[cellStatusLabel setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", tempActivity.percentageDone, tempActivity.totalMBUploaded, tempActivity.totalMBToUpload]];
tempActivity.referencingProgressBar = progressbar;
tempActivity.referencingStatusTextLabel = cellStatusLabel;
return cell;
}
As you can see, this is where I think I'm doing something which isn't quite good enough:
tempActivity.referencingProgressBar = progressbar;
tempActivity.referencingStatusTextLabel = cellStatusLabel;
The upload activities get a reference to the controls stored in this cell, and can then update them themselves. The problem is that I do not know whether this is safe or not. What if the cell they are refering to gets re-used or deleted from memory, and so on?
Is there another way in which you can simply update the underlying model (my upload activites) and then force the UI table view to redraw the changed cells? Could you eventually subclass the UITableViewCell and let them continously check up against an upload and then make them upload themselves?
EDIT
This is how the upload activity objects calls their referencing UI controls:
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
if (referencingProgressBar != nil)
{
[referencingProgressBar setProgress:(percentageDone / 100) animated:YES];
}
if (referencingStatusTextLabel != nil)
{
[referencingStatusTextLabel setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", percentageDone, totalMBUploaded, totalMBToUpload]];
}
}
My only concern is that, since these objects run asynchrounously, what if at some given point the UI table view decides to remove or re-use the cells which these upload objects are pointing to? It doesn't seem very secure at all.
There are two possibilities, assuming you have a background process that is uploading:
The tableview is a delegate and implements some uploadProgress
function
The tableview listens for uploadProgress NSNotifications
The second is easier to implement, just put the listeners start/stop in viewdidappear/viewdiddissappear. Then in your upload you can track progress and emit a notification with attached userinfo that gives an integer value to progress. The table has a function that handles this notification being received and redraws the cells. Here is how to add data to the userinfo part of an NSNotification.
If you wanted to be fancier you could have an upload id and map this to a cell index, and only redraw that particular cell. Here's a question and answers that explain how to do this.
Disgusting Pseudocode Since I don't have access to my IOS dev env right now
upload function:
uploadedStuff{
upload_id = ... // unique i, maps to row in table somehow
byteswritten = ...
bytestotal = ....
userinfo = new dict
userinfo["rowid] = upload_id
userinfo["progress"] = (int)byteswritten/bytestotal
sendNotification("uploadprogress",userinfo)
}
tableview.m:
viewdidappear{
listenForNotification name:"uploadprogress" handledBy:HandleUploadProgress
}
viewdiddisappear{
stoplisteningForNotification name:"uploadprogess"
}
HandleUploadProgess:NSNotification notification {
userinfo = [notification userinfo]
rowId = [userinfo getkey:"rowId"]
progress = [userinfo getkey:"rowId"]
// update row per the link above
}

Resources