I have a viewController in an app that retrieves Data from JSON, parses it and populates in UITableView. I am using thread to load data so that app does not hang when it is retrieving data.
Problem:
numberOfRowsInSection returns 0 and UITableView does not get populated sometimes when app is started. While sometimes, everything works fine. It is all random :S
Possible Explanation:
The problem is, it seems like, sometimes numberOfRowsInSection is called before data is retrieved. numberOfRowsInSection returns the value of count of a NSMutableArray called 'subjects'. Objects in 'subjects' are added when loadData is called. So the numberOfRowsInSection should return the count of 'subjects' and it should not be called after 'subjects' is populated.
Sometimes when I start the app, numberOfRowsInSection is called after 'subjects' is populated and UITableView shows data but sometimes when I start the app, numberOfRowsInSection is called before 'subjects' is populated and UITableView shows no data.
Code:
Here is my code:
-(void)loadData:(id)sender
{
dispatch_queue_t getRemindersQueue=dispatch_queue_create("reminders JSON downloader with reload Button", NULL);
dispatch_async(getRemindersQueue, ^{
[self getReminders];
dispatch_async(dispatch_get_main_queue(), ^{
self.navigationItem.rightBarButtonItem=sender;
[self.tableView reloadData];
});
});
dispatch_release(getRemindersQueue);
}
-(void)getReminders
{
NSURL * aURL = [NSURL URLWithString: #"http://www.merrycode.com/apps/IELTS/RemindersJSON"];
NSURLRequest *request = [NSURLRequest requestWithURL:aURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0];
NSError *responseError=nil;
// Perform request and get JSON back as a NSData object
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&responseError];
if(responseError)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *parsingError = [[UIAlertView alloc] initWithTitle:#"Network Error"
message:#"Can not reach the servers. Make sure you are connected to the internet."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[parsingError show];
});
return;
}
NSString *str = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
NSLog(#" String of Reminders JSON: %#",str);
NSString *newStr= [self stringByRemovingControlCharacters:str];
response = [newStr dataUsingEncoding:NSUTF8StringEncoding];
NSError *jsonParsingError = nil;
NSArray *publicTimeline = [NSJSONSerialization JSONObjectWithData:response options:0 error:&jsonParsingError];
NSLog(#"%#", jsonParsingError);
NSLog(#" publicTimeline Array Count: %d", [publicTimeline count]);
if([publicTimeline count] == 0)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *parsingError = [[UIAlertView alloc] initWithTitle:#"Error Retriving Data"
message:#"There was an error reciving data from the server."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[parsingError show];
});
return;
}
NSDictionary *colleges;
for(int i=0; i<[publicTimeline count];i++)
{
colleges= [publicTimeline objectAtIndex:i];
NSLog(#"Reminders: %#", [colleges objectForKey:#"title"]);
[self.subjects addObject:[colleges objectForKey:#"title"]];
[self.dates addObject:[colleges objectForKey:#"date"]];
[self.description addObject:[colleges objectForKey:#"desc"]];
}
[self.subjectsInNSUserDefaults removeAllObjects];
[self.datesInNSUserDefaults removeAllObjects];
[self.descriptionInNSUserDefaults removeAllObjects];
[self.userDefaults setObject:self.subjects forKey:#"SUBJECTS"];
[self.userDefaults setObject:self.dates forKey:#"DATES"];
[self.userDefaults setObject:self.description forKey:#"DESCRIPTION"];
[self.userDefaults synchronize];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"Array Count in numberOfRowsInSection: %d",[self.subjects count]);
return [self.subjects count];
}
Couple of ideas...
You said this is a UIViewController (as opposed to UITableViewController). How are you setting up your UITableView and the delegate? If you are still setting up your UITableView or its delegates while this background thread is running, it is theoretically possible that the background thread could complete before you are done setting up your UITableView, which could create strange issues (and explain why this happens "randomly").
Also, have you checked to make sure your response object is populated with information about colleges in the cases where your UITableView isn't getting populated (and not some sort of other response, or no response at all)? I see where you are checking for response errors, but you seem to assume that if there isn't an error, it will be a response with information about colleges (which may or may not be a safe assumption).
If you're correct about the data retrieval being the problem, then I have also had this problem. My inelegant solution was just to set a timer to populate the table at a time when I knew the data would be loaded.
Related
I am making a call to twitters API to load some tweets for a specific section of my app.
A small chunk of users are reporting a crash when loading the tweets view, while the rest have no problem at all.
I have submitted the code to Apple Tech Support and they responded letting me know that NSJSONSerialization can sometimes return a NSArray or NSDictionary.
Obviously it will throw an error is objectAtIndex: is called on an NSDictionary object, which I believe is the culprit for all of my users.
The partial solution is to detect if it is an Array or NSDictionary.
Here is where I am at now:
id feedData = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonError];
if ([feedData isKindOfClass:[NSArray class]]) {
//Is array
} else if ([feedData isKindOfClass:[NSDictionary class]]) {
//is dictionary
}
I basically need an NSArray every single time. So in the is array block, I basically just use the feedData, but in NSDictionary, how can I convert it to an NSArray that will match the structure I need.
Honestly the biggest issue is that I cannot see what the NSDictionary structure looks like because none of my testing devices or simulator return the NSDictionary data, they all return an NSArray.
Here is what the entire getUserFeed method that sends the request to twitter looks like:
// Get the twitter feed
NSURL *requestURL = [NSURL URLWithString:TW_API_TIMELINE];
// Set up proper parameters
NSMutableDictionary *timelineParameters = [[NSMutableDictionary alloc] init];
[timelineParameters setObject:kNumTweets forKey:#"count"];
[timelineParameters setObject:#"1" forKey:#"include_entities"];
// Create the Social Request
SLRequest *postRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:requestURL parameters:timelineParameters];
postRequest.account = self.delegate.userAccount;
// Perform the request
[postRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
// Check if we reached the reate limit
if ([urlResponse statusCode] == 429) {
// Rate limit reached
// Display an alert letting the user know we have hit the rate limit
UIAlertView *twitterAlert = [[UIAlertView alloc] initWithTitle:kRateLimitTitle
message:kRateLimitMessage
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[twitterAlert show];
// Stop animating the pull to refresh if it is animating
[self.feedTableView.pullToRefreshView stopAnimating];
return;
}
// Check if there was an error
if (error) {
NSLog(#"Error: %#", error.localizedDescription);
// Stop animating the pull to refresh if it is animating
[self.feedTableView.pullToRefreshView stopAnimating];
return;
}
// Check if there is some response data
if (responseData) {
NSError *jsonError = nil;
id feedData = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableContainers error:&jsonError];
if ([feedData isKindOfClass:[NSArray class]]) {
//Is array
NSLog(#"It's an Array");
} else if ([feedData isKindOfClass:[NSDictionary class]]) {
//Is dictionary
NSLog(#"It's a Dictionary");
} else {
//is something else
}
if (!jsonError) {
[self gatherTweetsFromArray:feedData];
} else {
// Stop animating the pull to refresh if it is animating
[self.feedTableView.pullToRefreshView stopAnimating];
// Alert the user with the error
UIAlertView *twitterAlert = [[UIAlertView alloc] initWithTitle:kErrorTitle
message:kErrorMessage
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[twitterAlert show];
}
} else {
// Stop animating the pull to refresh if it is animating
[self.feedTableView.pullToRefreshView stopAnimating];
// Alert the user with the error
UIAlertView *twitterAlert = [[UIAlertView alloc] initWithTitle:kErrorTitle
message:kErrorMessage
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[twitterAlert show];
}
});
}];
This is a MAJOR bug and I need to squash it, so any ideas or information will be greatly appreciated! Thank you!
I'm working with Core Data and web service, I want to add my data to my table,
but I don't know how should I call them, would you please help me, since when I used this way it's not working.
Here is my method for update database in my HTTP class
- (void)updateLocalCardsDataBase:(NSArray*) cardsArray
{
//check if current user has cards in local database
NSManagedObjectContext* managedObjectContext = [(AppDelegate*) [[UIApplication sharedApplication] delegate] managedObjectContext];
for(NSDictionary *cardDic in cardsArray)
{
Card *card = [NSEntityDescription insertNewObjectForEntityForName:#"Card" inManagedObjectContext:managedObjectContext];
card.remote_id = [NSNumber numberWithInt:[[cardDic objectForKey:#"id"] intValue]];
card.stampNumber = [NSNumber numberWithInt:[[cardDic objectForKey:#"stampNumber"] intValue]];
card.createdAt = [NSDate dateWithTimeIntervalSince1970:[[cardDic objectForKey:#"createdAt"] intValue]];
[managedObjectContext lock];
NSError *error;
if (![managedObjectContext save:&error])
{
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
NSLog(#"Failed to save to data store: %#", [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
else {
NSLog(#" %#", [error userInfo]);
}
}
[managedObjectContext unlock];
}
Here is my table:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
// NSManagedObjectContext* managedObjectContext = [(AppDelegate*) [[UIApplication sharedApplication] delegate] managedObjectContext];
static NSString *CellIdentifier = #"CardsCell";
CardCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil){
NSArray *objects = [[NSBundle mainBundle] loadNibNamed:#"CardCell" owner:nil options:nil];
for (id currentObject in objects)
{
if([currentObject isKindOfClass:[UITableViewCell class]])
{
cell = (CardCell *) currentObject;
break;
}
}
NSDictionary *f = [_cards objectAtIndex:indexPath.row];
cell.stampId.text = [f objectForKey:#"stampNumber"];
NSLog(#"%#fdssfdfddavds",[f objectForKey:#"stampNumber"]);
cell.createdAt.text = [f objectForKey:#"createdAt"];
cell.CardId.text = [f objectForKey:#"id"];
return cell;
}
Edit:
My problem is how I can show data in a UITableView
Before call [tableView reloadData], you need to get a data source first. You will get back an array of your data models, not an NSDictionary. You can place the my example method (or a variation that suits you best) where ever best suits your needs, but this one will not filter or sort the models, it will only get all of them. Also, I will place the method in your view controller that stores the table view:
-(NSArray*)getMycards {
NSManagedObjectContext *context = [(AppDelegate*) [[UIApplication sharedApplication] delegate] managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Card" inManagedObjectContext:context];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSError *error;
[request setEntity:entityDescription];
NSArray *cards = [context executeFetchRequest:request error:&error];
// now check if there is an error and handle it appropriatelty
// I usually return 'nil' but you don't have if you don't want
if ( error != nil ) {
// investigate error
}
return cards;
}
I recommend creating a property #property NSArray *cards in the view controller where you place your table, it will be easier to manage. One assumption I have made (since I have no other information about your view controller, a property named 'tableView' is declared in your view controller's header file (#property UITableView *tableView;), adjust the naming as needed.
With the above method, when you want to populate your array before loading the table's data:
// you put this block of code anywhere in the view controller that also has your table view
// likely in 'viewDidLoad' or 'viewDidAppear'
// and/or anywhere else where it makes sense to reload the table
self.cards = [self getMyCards];
if ( self.cards.count > 0 )
[self.tableview reloadData];
else {
// maybe display an error
}
Now, your cellForRowAtIndexPath should look like
-(UITableViewCell*tableView:tableView cellForRowAtIndexPath {
UITbaleViewCell *cell = ...;
// creating the type of cell seems fine to me
.
.
.
// keep in mind I don't know the exact make up of your card model
// I don't know what the data types are, so you will have to adjust as necessary
Card *card = self.cards[indexPath.row];
cell.stampId.text = [[NSString alloc] initWithFormat:#"%#",card.stamp];
cell.createdAt.text = [[NSString alloc] initWithFormat:#"%#",card.createdAt];
// you might want format the date property better, this might end being a lot more than what you want
cell.CardId.text = [[NSString alloc] initWithFormat:#"%#",card.id];
return cell;
}
Core Data is extremely powerful, I highly recommend the Core Data overview, followed by the Core Data Programming Guide.
I'm parsing my json file and showing it in a grouped table view.
-(void) connectionDidFinishLoading:(NSURLConnection *)connection
{
NSDictionary *allDataDictionary = [NSJSONSerialization JSONObjectWithData:webData options:0 error:nil];
NSDictionary *parks = [allDataDictionary objectForKey:#"Parks"];
NSArray *arrayOfParks = [parks objectForKey:#"Park"];
for (NSDictionary *diction in arrayOfParks) {
NSString *hebName = [diction objectForKey:#"hebName"];
NSString *engName = [diction objectForKey:#"engName"];
NSString *latitude = [diction objectForKey:#"lat"];
NSString *longtitude = [diction objectForKey:#"long"];
[array addObject:hebName];
}
[[self myTableView] reloadData];
}
-(void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *details = [array objectAtIndex:indexPath.row];
[[[UIAlertView alloc] initWithTitle:#"B7Tour" message:details delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
}
It works very well and I present in my table view the engName.
Clicking on the item at the table view will pop up an alert with the engName itself.
My question is: how to extract the particular latitude and longtitude and save them?
I know how to present them in the table view but I want to extract and save them for additional usage.
Any ideas?
You might want to have the array of dictionaries exist outside this method.
Then you can access them any time.
Then you can maintain another ivar that is your current selection in the table view.
From there it is pretty straight forward to call the objectForKey: on the currently selected object.
I am attempting to write a bit of code that checks the URL of a datasource, then populates an array with objects from that URL. It actually works well, but if there is a problem with the web connection or the address I want to populate the array with data from a bundled file. The issue I am having is that the connection didFailWithError method is never called. I tried passing a simple string but it does not call. I want the app to still function for people who are using ipod touch or are in airplane mode.
connection didReceiveResponse is working without issue.
This is what I'm working with.
- (void)loadListData{
NSLog(#"Loading data from sources");
NSURLRequest *listURLRequest = [NSURLRequest requestWithURL:integerPhoneListURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:1.0];
[[NSURLConnection alloc] initWithRequest:listURLRequest delegate:self];
if (!listConnectFail){
phoneListJSON =[NSData dataWithContentsOfURL:integerPhoneListURL];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:phoneListJSON waitUntilDone:YES];
} else {
//This will tell us if there is an error loading the file
NSLog(#"File not found on web init from file");
phoneListJSON =[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"contactlist" ofType:#"json"]];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:phoneListJSON waitUntilDone:YES];
}
//Initialize the filtered list with array of customer objects. Based on original data
filteredList = [[NSMutableArray alloc] init];
for (NSDictionary *dict in phoneListOriginal) {
contact *single = [[contact alloc] init];
single.fName = [dict objectForKey:#"fName"];
single.lName = [dict objectForKey:#"lName"];
single.extension = [dict objectForKey:#"extension"];
single.title = [dict objectForKey:#"title"];
single.department = [dict objectForKey:#"department"];
single.cellNumber = [dict objectForKey:#"cellNumber"];
//NSLog(#"%#", single.lName);
[filteredList addObject:single];
}
NSLog(#"Array filteredLIst contains %d records",[filteredList count]); }
-(void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
listConnectFail = YES;
NSLog(#"Connection Failed, pulling from file"); }
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
listConnectFail = NO;
NSLog(#"Connection Succeeded, populating from API");
}
I know it is probably something stupid that I am not seeing, but I could use the help to see what I don't
Thanks in advance!
How did you confirm that your delegate did not receive the message? Did you check the log?
Your code seems to assume that 'listConnectFail' will be set immediately after the NSURLConnection's init is done, which is not necessarily the case.
[[NSURLConnection alloc] initWithRequest:listURLRequest delegate:self];
if (!listConnectFail){...}
The NSURLConnection documentation states that 'The delegate will receive delegate messages as the load progresses.'
However, I am not sure about the airplane mode, maybe this particular error can be detected synchronously.
I'm working on my first JSON example in objective-c and came across this great tutorial that I'm trying to reproduce. Along the way I decided to push the JSON returned into my already working tableView (just to ensure I could do something w/ the data in the view).
- (void)viewDidLoad {
[super viewDidLoad];
responseData = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.unpossible.com/misc/lucky_numbers.json"]];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[connection release];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
[responseData release];
NSArray *luckyNumbers = [responseString JSONValue];
NSMutableString *text = [NSMutableString stringWithString:#"Nums "];
for (int i = 0; i < [luckyNumbers count]; i++)
[text appendFormat:#"%#", [luckyNumbers objectAtIndex:i]];
self.movies = [[NSArray alloc] initWithObjects:#"First", text, #"Last", nil];
}
What I've found is that when I set the array in "connectionDidFinishLoading" it shows up as nothing in the running application - yet if I set this directly in the "viewDidLoad" method with 3 simple string values it shows up fine.
When I debug the running application I see the JSON response and the string looks valid (no issues that I can see).
Is the datasource for my tableView already set in stone before this "connectionDidFinishLoading" method or did I miss something?
Your UITableView will call upon its DataSource for data once initially, presumably sometime after viewDidLoad. After that first load, it will only request data as it needs it (i.e. as you scroll to different cells.) If you want to make it refresh its contents when your data is ready (like after you've received your URL data), call [tableView reloadData].
My initial question was solved by this solution:
At the end of my "connectionDidFinishLoading" method I call a method on the appDelegate called "jsonFinished".
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//do all the json work and set the array that I'm using as my datasource
self.movies = [[NSArray alloc] initWithObjects:#"First", "Last", nil];
[appDelegate jsonFinished]; //have the app delegate do the refresh call back
}
Then inside the appDelegate I simply provide an implementation for the "jsonFinished" method that does a refresh of the UITableView
- (void)jsonFinished
{
moviesController.refreshDisplay;
}
And in the "refreshDisplay" method I do the reloadData on the tableView
- (void)refreshDisplay
{
[moviesTableView reloadData];
}
And now after the data is loaded the appDelegate fires off the method that reloads the data for tableView