populate UITableView - objective-c

I am struggling with a UITableView that I previously had working and somehow I have broken it!
It is part of a unit in Paul Hegarty's course
The symptom is that the view loads but it is empty. I am clearly misunderstanding something fairly basic.
So far as I understand the two key methods are 1 the section rows count which in my case is returning zero, which I know is wrong!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// #warning Incomplete method implementation.
// Return the number of rows in the section.
NSLog(#"TopPlaces %#",self.topPlaces);
//return 100;
return [self.topPlaces count];
}
Because of the above the following method is never called because there are no rows.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
And the second is within ViewDidLoad where I can log my data to the console and everything appears fine. i.e. my data is generated within ViewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t dowloadQueue = dispatch_queue_create("flick downloader", NULL);
dispatch_async(dowloadQueue, ^{
NSArray *topPlaces = [FlickrFetcher topPlaces];
//NSLog(#"Array is %#",topPlaces); // array is OK here
dispatch_async(dispatch_get_main_queue(), ^{
NSSortDescriptor *woeDescriptor = [[NSSortDescriptor alloc] initWithKey:#"_content" ascending:YES];
NSArray *woeDescriptors = #[woeDescriptor];
NSArray *sortedReturns = [topPlaces sortedArrayUsingDescriptors:woeDescriptors];
self.topPlaces = sortedReturns;
//all the data is present here, count is 100 and array will log to console
NSLog(#"count here is %u",[self.topPlaces count]);
});
});
// Uncomment the following line to preserve selection between presentations.
self.clearsSelectionOnViewWillAppear = NO;
}

The problem is that you make an asynchronous call to fetch the data (which means that your array should be full of data at some point in the future), but you do not reload your tableview afterwards. Calling reloadData will do the trick:
...
self.topPlaces = sortedReturns;
//all the data is present here, count is 100 and array will log to console
NSLog(#"count here is %u",[self.topPlaces count]);
[self.tableView reloadData]; // Assuming that 'tableView' is your outlet
This will instruct your tableview to query once again its datasource and will eventually load all the data in your (now non-empty) topPlaces array.
Further Explanation:
I saw in the comment of #nerak99 that he is not completely sure why the problem fixed with reloadData.
Well, let's use an example:
Imagine that you've got a restaurant.
You open the place at 06:00 in the morning and you see that you've got nothing to cook. So you ask from one of your guys to go to the market for supplies (that's your asynchronous call).
At the same time you instruct a waitress to write today's menu, so she writes... well, nothing (that's your tableview asking for number of rows).
Now at 07:00 the guy who went to the market returns with 10 items. What is the next logical step in order to update your menu? To actually inform the waitress (that's your reloadData) about the items you've returned with.
I hope that this makes sense :)

What is self.topPlaces? Try to NSLog the array and see if there's any contents. If not, make sure that it's being set.
I'll be able to write a more specific answer if you provide more info.

Related

How Can I Pass Data from Array in didLoadObjects to cellForRowAtIndexPath

So, I'm using RestKit to access a webservice and retrive data from there.
So far I have two views, and that part of retrieving the data is fine, and I get the 100 objects I need, saving them into an array called "songs".
Here is didLoadObjects:
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
NSLog(#" Reached didLoadObjects: %d", [objects count]);
self.songs = objects;
NSLog(#"%#",self.songs);
}
Ok, I have two views, and the problem is, of course, on the second one. I'm using Storyboards. I gave the tableView cell an identifier called "TopListCellIdentifier".
So, I get the objects, all the 100 of them are "printed" in the command line, but the problem starts when I try to access the data from the array inside cellForRowAtIndexPath, something which has to be done because I have a custom tableViewCell displaying the info I need (sons, artists, covers, stuff like that). So, when I start the app, the first view is fine, but the second has 100 cells, but no info. Here is the cellForRowAtIndexPath code.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *TopListCellIdentifier = #"TopListCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TopListCellIdentifier];
// Loading the Cover Images in the Background
// Cover Image: Tag 1
[((HJManagedImageV*)[cell viewWithTag:1]) clear];
HJManagedImageV* thumbImage = ((HJManagedImageV*)[cell viewWithTag:1]);
NSString *thumbUrl = [[self.songs objectAtIndex:[indexPath row]] thumbnail];
thumbImage.url = [NSURL URLWithString:thumbUrl];
[[ImageHandler sharedHandler].imgManager manage:thumbImage];
//Song and Artist: Tag 2 and 3
((UILabel*)[cell viewWithTag:2]).text = [[self.songs objectAtIndex:[indexPath row]] title];
((UILabel*)[cell viewWithTag:3]).text = [[self.songs objectAtIndex:[indexPath row]] artist];
//Arrow Up/Down/Same: Tag 4
//TODO
//Position Number: Tag 5
((UILabel*)[cell viewWithTag:5]).text = [NSString stringWithFormat:#"%d.", [indexPath row]+1];
return cell;
}
I've tried to put a debugger in the first line of cellRow(...) but the program doesn't enter there. I feel like i'm forgetting about something very simple, but I can't seem to figure out what.
Can someone help me, please?
You're never initing a new cell. You need to add a line like :
if (nil == cell)
cell = [[UITalbeViewCell alloc] init...
after your call to
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TopListCellIdentifier];

Assertion failure in -[UITableView _endCellAnimationsWithContext:]

Hopefully this will be a quick fix. I have been trying to figure out the error that i keep getting. The error is listed below and the appdelagate is below that.
Any help is appreciated.
Thanks
2012-04-12 21:11:52.669 Chanda[75100:f803] --- Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-04-12 21:11:52.671 Chanda[75100:f803] --- Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
#import "AppDelegate.h"
#implementation AppDelegate
#synthesize window = _window;
#synthesize databaseName,databasePath;
- (BOOL)application: (UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions {
self.databaseName = #"Customers.db";
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDir = [documentPaths objectAtIndex:0];
self.databasePath = [documentDir stringByAppendingPathComponent:self.databaseName];
[self createAndCheckDatabase];
return YES;
}
- (void)createAndCheckDatabase {
BOOL success;
NSFileManager *fileManager = [NSFileManager defaultManager];
success = [fileManager fileExistsAtPath:databasePath];
if (success) return;
NSString *databasePathFromApp = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:self.databaseName];
[fileManager copyItemAtPath:databasePathFromApp toPath:databasePath error:nil];
}
#end
I don't see the reason for you to show us this part of code. Your error must be connected to this part in your code I assume
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
Probably you are making a mistake in one of these data source methods. Currently it's impossible to say what exactly is wrong but I assume it could be something like: You are telling the table view in the numberOfRowsInSection you would like to have n rows reserved and setup and in the cellForRowAtIndexPath you then only handle n - 1 rows for example.
Sorry that this answer can't be as precise as it should be. If you show us your implementation of your data source it would be much easier to tell what's going on.
Like Sun Tzu said: it's best to win without fighting. In my case whenever I see this kind of error message (ie discrepancy between rows added deleted etc).. I don't even debug anything.. I simply avoid making that extra call where I reload the rows etc.. that's 99% of the cases where this error happens.
This is a common scenario where this bug happens: I have a UINavigationController and it has a UITableView, when I click on a row it pushes a new UITableView and so on. This error always happens to me when I pop the last UITableview and go back to the UITableView before it, at this point I make an unnecessary call to the loadIt function which basically inserts the rows and relaods the UITableView.
The reason this happens is because I erroneously place my loadIt function in viewDidAppear:animated rather than viewDidLoad. viewDidAppear:animated is called every time the UITableView is displayed, viewDidLoad is only called once.
When removing rows, remember that it also checks sections when updating, in:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView
If you want to remove a row that is the last item in a section you need to remove the whole section instead (otherwise it might get section count wrong and throw this exception).
Don't forget to update your array which determines numberOfRowsInSection. It needs to be updated before you animate and remove
We check if number of rows in section is 1 because we will have to delete the entire section.
Do correct me if anyone can make this answer clearer.
[self.tableView beginUpdates];
if ([tableView numberOfRowsInSection:indexPath.section] == 1) {
[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
} else {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView endUpdates];
I put each section elements in separated arrays. Then put them into another array( arrayWithArray). My solution here for this problem:
[quarantineMessages removeObject : message];
[_tableView beginUpdates];
if([[arrayWithArray objectAtIndex: indPath.section] count] > 1)
{
[_tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indPath] withRowAnimation:UITableViewRowAnimationBottom];
}
else
{
[_tableView deleteSections:[NSIndexSet indexSetWithIndex:indPath.section]
withRowAnimation:UITableViewRowAnimationFade];
}
[_tableView endUpdates];
I had the same error.
I was using the following lines
UINib *myCustomCellNib = [UINib nibWithNibName:#"CustomNib" bundle:nil];
[tableView registerNib:myCustomCellNib forCellReuseIdentifier:#"CustomNib"];
to register the nib in the viewDidLoad method, since I had a different nib that was also associated with the same class. Hence, the line
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"GBFBLoadingCell"];
was returning nil unless I registered the nib in the viewDidLoad.
My problem was that I forgot to set the identifier in the attributes inspector for my file "CustomNib.xib" and "CustomNib~iphone.xib". (Or more precisely, that I forgot to press enter after typing the identifier in the attribute inspector in XCode, so that the new name failed to save.)
Hope this helps.
If you're using an NSFetchedResultsController like me and updating data in a background thread, don't forget to begin and end updates in the delegate:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
I Had the same error , which when trying with [tableView reloadData] was working fine .
The error was actually in the line
[TabView insertRowsAtIndexPaths:indexPathsArray withRowAnimation:UITableViewRowAnimationRight];
When i tried to check the indexPath values , they weren't correct as required .
I fixed it by changing values in indexPathsArray .
Hope this helps .
Its could be one of UITableViewDataSource protocol methods
For
- tableView:numberOfRowsInSection:
it should return an integer equal to the sum or result of
-insertRowsAtIndexPaths:withRowAnimation: and/or -deleteRowsAtIndexPaths:withRowAnimation:
For
- numberOfSectionsInTableView:
it should return an integer equal to the sum or result of
-insertRowsAtIndexPaths:withRowAnimation: and/or
-deleteSections:withRowAnimation:
I had the same problem with a Core Data base. If your using many FRC, you just need to reload the tableview inside each condition in numberOfSectionsInTableView.
I just had this happen to me while using Swift and a FRC backed data store to manage information. Adding a simple check to the delete operation to evaluate the current indexPath.section allowed me to avoid an extraneous call. I think I understand why this problem occurs... Basically I load a message into the top row whenever my dataset is empty. This creates an off by one issue as there is a faux row.
My Solution
... delete my entity, save the datastore and call reloadData on tableView
//next I add this simple check to avoid calling deleteRows when the system (wrongly) determines that there is no section.
if indexPath.section > 0 {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
Simply check that you call [yourTableView reloadData]; after modify array of values.

reloadRowsAtIndexPaths stops loading UITableViewCells?

I'm trying to incorporate favicons into a UITableView. The table basically fetches websites, and I want to display the favicon onto the right. I put a placeholder icon at the right initially and let a function in the background run. This function takes the URL of the website, parses it and attempts to find the favicon. If it can't find it, it keeps the same placeholder image; otherwise, it replaces it with the site's favicon. I initially tried using [tableView reloadData] which worked well in the simulator, but it did really odd and unreliable things (like for instance, it would create some cells, but then leave a giant, blank cell). Anyway, I stumbled upon reloadRowsAtIndexPaths, and it seems like the function I need to use. However, the results are still pretty unreliable. I have my fetching function running in the background as such:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...//other code here for showing website labels
//obtain the favicon.ico
if(!this_article.iconLoaded){
this_article.iconLoaded = TRUE;
NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:this_article, #"article", indexPath, #"indexPath", nil];
[self performSelectorInBackground:#selector(fetchFaviconWrapper:) withObject:args];
}
cell.favicon.image = this_article.icon;
return cell;
}
in FetchFaviconWrapper:
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[articleTable beginUpdates];
[articleTable reloadRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[articleTable endUpdates];
[pool release];
}
Basically, fetchFavicon takes a website, takes the host URL, appends "/favicon.ico", constructs it into an NSData object, and finds the image (if it exists). However, this has also been pretty unreliable. What would be the best way to replace the placeholder image while running a thread in the background? I could do everything on the main thread, but that just makes the table load slowly. There seems to be something that I'm overlooking, or something that I just forgot to add...just can't figure it out.
It is not entirely clear if you are accessing and modifying your UI from the separate thread.
If so, this is the cause of your unreliability. UIKit can be accessed only from the main thread. If you are interested you will find many questions on S.O. and many discussions on the web.
There is a workaround, if you want to keep your second thread. Indeed, you can send messages to your UI object using:
-performSelectorOnMainThread:withObject:waitUntilDone:
instead of sending them directly from the secondary thread.
If this workaround does not solve the issue for you, then I would suggest redesigning your app so that the secondary thread only access your model, without accessing the UI. All the operations that modify the UI should be executed on the main thread. If you need to call reloadData on your table when the model is ready, you can do it using performSelectorOnMainThread:withObject:waitUntilDone.
I'd suggest a few things.
Appending ./favicon.ico isn't always accurate. Look into various methods of adding favicons to sites to support them.
As far as replacing the default placeholder, I suggest using the NSNotificationCenter to inform the main thread when to make the changes.
I definitely reiterate the no UI from background thread.
change your initial code:
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[articleTable beginUpdates];
[articleTable reloadRowsAtIndexPaths:paths
withRowAnimation:UITableViewRowAnimationFade];
[articleTable endUpdates];
[pool release];
}
to this, and see if you're still having the problem
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[self performSelectorOnMainThread:#selector(reloadData)
withObject:nil
waitUntilDone:YES];
[pool release];
}
The waitUntilDone is irrelevant in this case, and it's preferred form to say YES.
Another problem to suspect when you have funky cell behavior is if you are correctly handling cell reuse. You didn't show us that code, so we can't tell. If you are keeping a reference to a cell somewhere and setting the image data into that - you'll hose yourself when the table reuses the cell out from under you. (I don't think that's the problem in this case.)

Objective C threads and GUI updates problem

I'm developing an iOS app with a view containing a TableView.
Some method receives data from the web, opens a new thread to calculate information and inserts a row into the table at run time with the method: insertRowsAtIndexPaths.
Now if a lot of data is coming at once, the table may update itself after a few insertions and not after each one, and thats provokes an exception saying that the number of rows in section isn't right (that's because it thinks it should have an increment of one row but the threads already inserted the array of data some more cells).
Even if I make a lock on the insertion to the datasource array and the insertRowsAtIndexPaths method, it's still do the same.
NSLock *mylock = [[NSLock alloc] init];
[mylock lock];
[array addObject:object];
[tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationLeft];
[mylock unlock];
help please,
Thank you!
you have to run this method on the main thread. All User Interface interaction has to be done on the main thread.
Let's say your method looks like this:
- (void)addSomeObject:(id)object {
[array addObject:object];
[tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationLeft];
}
and you are calling it like this:
[self addSomeObject:anObject];
then you would change this call to something like this:
[self performSelectorOnMainThread:#selector(addSomeObject:) withObject:anObject waitUntilDone:NO];

Navigating between View controllers?

In my Iphone application I am trying to navigate from one table view controller to next table view controller. Problem I am facing is that I have to fetch data using http request and then parse this data when the user select a cell. I am able to fetch and parse the data but the view controller is not waiting for the data to parsed and the next view controller is shown (which is empty). How to over come this problem.
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail : indexSelected];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
This is what you do:
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail:indexSelected delegate:self];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
You define a protocol that your view controller conforms to and when the fetching and parsing of data is done you call a method on the delegate to let the view controller know that the data is ready to be displayed.
If you need more information on how to do this, leave a comment.
EDIT: So here's how to declare and use a protocol. I'm going to try to keep it as simple as possible. I'm not sure if I like your naming convention, but I'll still use it for this example.
So let's get down to the code. This is how you declare a protocol:
#protocol MySingletonClassDelegate <NSObject>
#optional
- (void)didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:(NSArray *)data;
- (void)failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:(NSError *)error;
#end
Again, I'm not too fond of the naming convention. You shouldn't have underscores in objective-c method names.
The protocol should be defined in MySingletonClass.h before the declaration of MySingletonClass.
I declared two methods in the protocol, one for delivering the data and one for delivering an error if it fails, so that you can notify the user that it failed.
To use the protocol you need the following:
#interface SubscribersDetailViews2 : UITableViewController <MySingletonClassDelegate>
You also need to implement the methods declared in the protocol, but I'll leave that implementation to you.
Since the fetching of data already seems to be happening in the background I don't think I'll need to explain how to do that. One important thing to remember is that you want to execute the delegate methods on the main thread. Here's the code to do that:
- (void)doAnAuthenticatedAPIFetch_Subscriber_Detail:(NSUInteger)index delegate:id<MySingletonClassDelegate>delegate {
// Fetching data in background
if (successful) {
[self performSelectorOnMainThread:#selector(didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:) withObject:data waitUntilDone:NO];
} else {
[self performSelectorOnMainThread:#selector(failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:) withObject:error waitUntilDone:NO];
}
}
Just to be clear the // Fetching data in background is supposed to be replaced by your code. I assume that your code produces the variables (NSArray *data, NSError *error, BOOL successful) that I use.
That's about it, if you need clarification on anything let me know.
There are a number of options:
Cache the data, i.e., take a full copy of it on the iOS device (may not be practical of course)
Display an interstitial screen saying "loading" and then move to the "real" screen when the data has downloaded
Have, effectively, two different data sources for your table. The first is your current one. The second would be a single cell saying "Loading..."
In short, there's no point and click way of doing this but there's no problem downloading the data on the fly as long as you tell your users what's happening.