I went from using an NSURLConnection in my tableview controller to using an NSURLSession in a separate class with a callback that is processed in the tableview controller.
Now the data returned from my website does not get displayed in the populated table for up to 20 seconds even though it was loaded long ago.
I have an 'add' button on the navigation bar of tableview which brings up another view. When I click the add button, I can see the data in my main tableview is already populated as it animates to the next view. Returning back to the main view and the data is there.
I have tried implementing a number of ways to reload the data but they have no effect.
The old way which works fine, only lets me have one connection. I needed to have several connections available to call based on options I might have selected which is the reason for creating a new class to handle the connections and placing its callback in the tableview.
But this has created the problem of not being able to view the parsed return data immediately.
To me, it seems to be some type of threading issue, but I don't know how to troubleshoot it or how to correct it, so I am hoping someone here can suggest something to try.
Here is the applicable code in my new class we will call NetWorkClass for purposes of illustration...
#pragma mark - Get Parents
+ (void)requestParentsWithCompletionHandler:(RequestCompletionHandler)completionBlock {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:#"insert url here"]];
[request setHTTPMethod:#"POST"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig];
[[session dataTaskWithRequest:request completionHandler:completionBlock] resume];
}
This typdef is in the header file for my new class NetworkClass...
typedef void (^RequestCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error);
Then in the main thread of my tableview controller class I call a method that invokes the Network class to retrieve JSON data from a website.
The following snippet is called from ViewDidLoad in my tableview controller...
// Use NSSesssion to request JSON data from my website [self getParents];
NSLog(#"getParents has completed!");
// The view however, will not display for approximately 20 or 30 seconds unless I click on the Add button which instantiates another view // and then I see the data in the tableview right away as it animates to the new view. NSLog(#"parentsTable:%#",parentsTable);
NSLog outputs the following:
2014-02-11 16:10:27.385 myApp[12667:70b] parentsTable:; layer = ; contentOffset: {0, 0}>
The data has not yet been processed by the callback at this point, but the website has been sent the request and the repsonse is being returned.
The callback is implemented in getParents...
-(void)getParents
{
[NetWorkClass requestParentsWithCompletionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if (!error) {
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
if the response status code is 200, I propagate the data into the tableview and finish up by calling
[parentsTable reloadData];
All of the code following the statuscode check is identical to the code that works if I use an NSURLconnection (and its delegate methods) within the tableview to retrieve the data, so I have not included it here.
If I put a breakpoint at requestParentsWithCompletionHandler in getParents, and I single step from there, the first pass skips around my code. But if I then run from that point, it hits the breakpoint a second time and then falls thru to process the response, which is working as I would expect it since we have to wait a few milliseconds for the data to be obtained from the website.
The data has arrived within milliseconds, but it can take up to 20 seconds before the tableview fills in with data that was parsed if I do nothing.
Any help with this would be greatly appreciated.
You need to change your structure to the following:
In viewDidLoad You should call a method that will send an asynchronous request for the data. (strongly suggest using AFNetworking or restKit to do it instead of trying to get by with the code you are currently using)
On success you should populate an array with the data and [tableView reloadData]
You numberOfRowsInSection should look like the following:
If (!arrayContainingData) {
return 0; // For elegance, you can also return 1 and have a cell with an UIActivityMonitor
} else {
return arrayContainingData.count;
}
Finally use cellForRowAtIndexPath to build the cells.
Related
I have a view controller with labels, textfields, activity indicator and a button control all tied with IBOutlets and accessible from within my code.
When the user presses the button, I hide a few of the fields, and put up the activity indicator. I then make a synchronous URL call to get some JSON data.
The view controller is not updated to reflect the activity indicator and hidden fields until AFTER the synchronous request returns. I need this to happen before the request returns so the users sees that something is happening.
I have tried putting in a usleep(200000);, and also tried [self.view setNeedsDisplay]; - - both to no avail.
Is there any way I can force the screen update BEFORE the blocking synchronous call? I know I can go to an async call, but I really don't want to do that since I cannot do anything in the app until/unless I get the data i need...
Thanks,
Jerry
here is the code I use to send the sync request: This is the relevant portion of the routine 'SendGetEventsRequest'.
NSURLResponse *response = nil;
NSError *error = nil;
[self.aiActivityIndicator startAnimating];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
Here are the other routines i use right before the above routine gets called:
- (IBAction)btnGetEvents:(id)sender {
// The user presssed the GetEvents button. Send the requested server to be processed.
[self.aiActivityIndicator startAnimating];
[self.lblCollectingEventNames setHidden:NO];
[self.lblEnterServerName setHidden:YES];
[self.btnGetEvents setHidden:YES];
[self.tfServerName setHidden:YES];
[self.view setNeedsDisplay];
[self SendGetEventsRequest:self.tfServerName.text];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
[self.tfServerName resignFirstResponder];
[self btnGetEvents:self];
return YES;
}
Basically, when the user presses the 'go' button on the keyboard, the routine textFieldShouldReturn gets called. I dismiss the keyboard then simulate pushing the button 'Get Events'.
In btnGetEvents, I start up the activity indicator, hide/show a few fields, then call sendGetEventsRequest. In there is the code where i do the Sync call.
I am setting up the activity indicator and show/hide fields BEFORE the sync call, yet they are not updated until AFTER the sync call returns. I believe this is because the view controller did not perform a screen redraw before the Sync call got executed.
So, I need to figure out how to get the screen to update BEFORE the Sync call. I hope the additional code and additional explanation helps.
Wow, I cannot believe I am the only person to have this issue. I modified my code to use the async call and it is working perfectly.
I am developing a news application.I am using a table view to show the news. To download data from the server I am using sendAsynchronousRequest .
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil)
{
downloadedItem = [GNNewsItems saveDataToModel:data];
if ([self.delegate respondsToSelector:#selector(receivedResponse:)])
{
[self.delegate performSelectorOnMainThread:#selector(receivedResponse:) withObject:downloadedItem waitUntilDone:NO];
}
}
else if ([data length] == 0 && error == nil)
{
// Data not downloaded
}
else if (error != nil)
{
// error
}
}];
So far so good. Now consider a case:
User opens the app.
Table View send's a request to download the content of the first cell. Let us assume it takes 10 seconds to download the data.
User scrolls the table view to the 5th cell.
Table view sends the request for 5th cell.
Now user comes back to the first cell but the content of the initial request sent by cell 1 is not yet downloaded.
Table view will send a duplicate request for the first cell.
How can I cancel the duplicate request from the table view?
Create a NSMutableArray which will contain the indexPath of the cell for which request has already been initiated. Before initiating the web request for the a new in a cell check in the if request is already initiated or not. If not then initiate it else do nothing.
Your design description sounds like you are not using a standard Model/View design paradigm. You should store the headlines and other associated data, such as the full story or link to same in some kind of datastore, e.g. an array or CoreData etc. That object is what requests stories and updates data elements. Then it only does it once and in the background, and once gotten, it is done.
Then the tableView uses that datastore to populate table cells. It is a VERY bad design to be making over the air requests for stories each time a cell scrolls into view. It has the design issues you just mentioned plus it is very wasteful of users bandwidth and your server resources.
I have two ViewController and use a (tableview click) seque for opening the second ViewController.
My Problem is, the Second View Controller load much Data. So the time between switch is <> 10 Seconds. In this 10 Seconds the App freeze. Thats OK, but HOW can i insert a "Popup" or "Alert" Message like "Please Wait..." BEVOR . I have testing much tutorials for Popups and Alerts, but the Popup/Alter shows only, when the SecondView Controller is complete loaded. I will show the Message BEVOR the SecondViewController is compled loaded.
Example:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// IF i set here the ALERT, the Alter was only show, when the Second View Controller is complete loaded!
NSDictionary *rowVals = (NSDictionary *) [SearchNSMutableArray objectAtIndex:indexPath.row];
[self performSegueWithIdentifier:#"Foo" sender:self];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"Foo"]) {
// Get indexpath from Tableview;
NSIndexPath *indexPath = [self.SearchUITableView indexPathForSelectedRow];
// Get Data from Array;
NSDictionary *rowVals = (NSDictionary *) [self.SearchNSMutableArray objectAtIndex:indexPath.row];
// Destination View;
[MySecondViewController alloc];
MySecondViewController *MyView = (MySecondViewController *)segue.destinationViewController;
}
}
You are trying to fix the problem with the wrong solution. That solution is just as bad because the popup will also freeze for 10 seconds. What if you add more data and it takes 30 seconds or 10 minutes? Are you going to expect your users to see a dialog they can't dismiss for 10 minutes?
Are you fetching the data from the internet? If so you need to fetch your data asynchronously in the background.
If you're loading it from disk then there's too much being loaded that could possibly be displayed on one screen, you need to load only a small portion of it, and if that still takes a long time you need to load it asynchronously.
UPDATED -
You should have a model class for your application that is responsible for fetching the data from the internet.
Google Model View Controller to get some background information on what a Model is.
As soon as the app launches the model can start to download the data which needs to be down in the background (that's too big a topic to answer how to do that here).
The View controller can launch while the data is being downloaded and it can display a spinning activity indicator wheel or progress bar or dialog etc. while waiting. The important thing is the GUI will not freeze.
When the model has downloaded the data it needs to tell the view controller the data is now available, which it can do using NSNotification center.
There's lots for you to investigate and learn, to do it without GUI freezing it needs to be done properly, there's no shortcut, you have a lot to study.
#Martin,
i found a solution:
// Send the Request;
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
So the request are asynchrony. Thanks for your answer. Great +1
My app is getting a feed of JSON data quite big representing thousands of products data. After parsing and saving data in CoreData (all in background threads), displaying data in UITableView after parsing block the UI since it's done in the main thread. But the problem is that the UI remains blocked for few seconds which is not user friendly. So how do you suggest I can handle reloading data without blocking UI?
//...
//After parsing data, reload UITableView
[self.tView reloadData];
EDIT:
I may re-explain my issue, after parsing data, reloading data, UITableView object display all data. Then UI is blocked for few seconds before I can finally use the app again.
here is my relevant code:
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON){
//Parse data
[self.tView reloadData];//Display data
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
//Save data
}];
}failure:^(NSURLRequest *request, NSHTTPURLResponse *response,NSError *error, id JSON){
//
}];
[operation start];
}
Based on sangony comment, it turns out that the call of tableView:heightForRowAtIndexPath: delegate method affect performance especially with my case where I deal with over than 3000 rows. here is Apple documentation related to that:
There are performance implications to using tableView:heightForRowAtIndexPath: instead of
the rowHeight property. Every time a table view is displayed, it calls
tableView:heightForRowAtIndexPath: on the delegate for each of its rows, which can result in a
significant performance problem with table views having a large number of rows (approximately
1000 or more).
From other threads discussing similar issues, I can get rid of performance issues by assigning the row height on the table view property:
self.tView.rowHeight = 200.0;
And of course, remove the implementation of tableView:heightForRowAtIndexPath: otherwise it will override the rowHeight property. Note that this solve my problem only in case I deal with single row height. So this cannot help in case where I need to apply more than one row height value, where tableView:heightForRowAtIndexPath: is required.
Use NSThread to call a method in which you fetch Json data and after fetching, reload the table in the same method.
[NSThread detachNewThreadSelector:#selector(GetJsonDataAndReload) toTarget:self withObject:nil];
So,this process will not block user interaction.
We're having a problem displaying a view above a tableView on iOS. Our approach
is to create a UIView that is a subview of a sublass of UIViewController, send
it to the back, and then bring it to the front upon didSelectRowAtIndexPath.
We're using an XIB to create the user interface. The view hierarchy is like
this:
View
-- UIView ("loading..." view)
-- -- UILabel ("loading...")
-- -- UIActivityIndicatorView
-- UITableView
-- UILabel
Here is what we're doing to try to display the "loading" view:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Create a request to the server based on the user's selection in the table view
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
NSError *err;
// Show the "loading..." message in front of all the other views.
[self.view bringViewToFront:self.loadingView];
[self.loadingWheel startAnimating];
// Make the request
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&err];
// Stop animating the activity indicator.
[loadingWheel stopAnimating];
// other stuff...
}
Whenever we leave the "loading" view at the front of all the other views in the
XIB, we can see that it looks as we want. However, when we leave the loading
view at the back (per the view hierarchy above) and then try to bring it to the
front, the view never displays. Printing out self.view.subviews shows that our
loading view is in fact in the view hierarchy. Interestingly, if we try to
change something else in our view within didSelectRowAtIndexPath (for example,
changing the background color of a label that's already displaying in the view),
the change never shows on the simulator.
The problem is the synchronous request. It blocks the main thread, and so the activity indicator does not get a chance to show.
A simple solution would be to asynchronously load the data on a global queue, and when everything is loaded, call back to the main queue.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Make the request
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&err];
dispatch_async(dispatch_get_main_queue(), ^{
// Stop animating the activity indicator.
[loadingWheel stopAnimating];
// other stuff...
});
});
While the solution above works, it blocks a global queue and so it is not ideal. Have a look at the asynchronous loading via NSURLConnection. It is explained in great detail in Apple's "URL Loading System Programming Guide".