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.
Related
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 am using AFNetworking GET method in ViewDidLoad. My UITableView delegate methods runs before data is loaded in Arrays. I am getting error of NSArray beyond bounds .
Please help me through it . Its my first time o JSON .
I searched Stackoverflow and google .But didn't got proper answer.
You shouldn't be refreshing your table view before the download completes if you don't want it pulling in data from blank arrays.
You should be refreshing your data in the success block of your AFNetworking call.
[connectionMgr GET:#"yourURL" parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) { //Note that 204 is considered a success message
//Reload your table view
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) { //Note that this is called even if the download is cancelled manually
//Failure
}];
EDIT
Since you're using a UITableViewController, you should put a check in your numberOfRowsInSection to see if the array is nil or if it contains 0 objects. Then it won't try to generate any cells.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (array == nil || array.count < 1) {
return 0;
} else {
return array.count; //Or whatever you're using
}
}
I'm assuming that you aren't using array.count for the number of cells, otherwise you probably wouldn't be having this issue.
I would suggest having an NSArray property that will contain the data you are going to put in the table view. numberOfRowsForSection: return the count of the array. In your success block, set the array equal to the data you have returned & call reloadData. This way your table view will try creating 0 cells when there's no data & as many as you need after you have received the data.
I have a UITableView (TV) with several sections, each section has an NSArray that serves as the dataSource (no CoreData, no images). When the user opens the TV, my app does some intensive calculations to generate the dataSource arrays. In some cases, the calculations can take some time, and what happens then is that the section headers show first, after which the cells appear, which doesn't like good, I think.
I'm already using GCD to do the calculations:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
[MBProgressHUD showHUDForView: self.view animated: YES];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self.model generateData];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView: self.view animated: YES];
[self.tableView reloadData];
});
});
}
Besides trying to optimize the calculations, is there anything else I could do to make this look smoother? For instance, is there a way for the section headers not to appear until the calculations are done?
UPDATE:
So in the end, my solution turned out to be different. To generate my data I am now using a dispatch_group, and calculate theNSArray for each section in andispatch_group_async block, so they run concurrently. This already was an improvement in speed. Furthermore, I start the calculation already in the UIViewController from which the user opens the TV. Therefore, the data is available almost instantly when the TV opens, and all sections load smoothly.
Here is a code snippet for completeness:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ^{
[self.model generateArray1];
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ^{
[self.model generateArray2];
});
//... etc for each section
// make sure that everything is done before moving on
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
If you return nil from tableView:titleForHeaderInSection: then the header won't be shown, so add a small amount of conditional logic which checks if the data is loaded yet and either returns nil (if not loaded) or the section title (if it is loaded).
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.
As the title implies, my UICollectionView doesn't update and display the cells immediately after calling reloadData. Instead, it seems to eventually update my collection view after 30-60 seconds. My setup is as follows:
UICollectionView added to view controller in Storyboard with both delegate and dataSource setup for the view controller and standard outlet setup
numberOfSectionsInRow & cellForItemAtIndexPath are both implemented and reference the prototyped cell and the imageView inside of it
Here is the code that goes to Twitter, get's a timeline, assigns it to a variable, reloads a table view with the tweets and then goes through the tweets to find photos and reloads the collection view with those items.
Even if I comment out the code to display the image, it still doesn't change anything.
SLRequest *timelineRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:timelineURL parameters:timelineParams];
[timelineRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if(responseData) {
JSONDecoder *decoder = [[JSONDecoder alloc] init];
NSArray *timeline = [decoder objectWithData:responseData];
[self setTwitterTableData:timeline];
for(NSDictionary *tweet in [self twitterTableData]) {
if(![tweet valueForKeyPath:#"entities.media"]) { continue; }
for(NSDictionary *photo in [[tweet objectForKey:#"entities"] objectForKey:#"media"]) {
[[self photoStreamArray] addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[photo objectForKey:#"media_url"], #"url",
[NSValue valueWithCGSize:CGSizeMake([[photo valueForKeyPath:#"sizes.large.w"] floatValue], [[photo valueForKeyPath:#"sizes.large.h"] floatValue])], #"size"
, nil]];
}
}
[[self photoStreamCollectionView] reloadData];
}
}];
This is a classic symptom of calling UIKit methods from a background thread. If you view the -[SLRequest performRequestWithHandler:] documentation, it says the handler makes no guarantee of which thread it will be run on.
Wrap your call to reloadData in a block and pass this to dispatch_async(); also pass dispatch_get_main_queue() as the queue argument.
You need to dispatch the update to the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self.photoStreamCollectionView reloadData];
});
or in Swift:
dispatch_async(dispatch_get_main_queue(), {
self.photoStreamCollectionView.reloadData()
})
Apple say:You should not call this method in the middle of animation blocks where items are being inserted or deleted. Insertions and deletions automatically cause the table’s data to be updated appropriately.
In face: You should not call this method in the middle of any animation (include UICollectionView in the scrolling).
so, you can:
[self.collectionView setContentOffset:CGPointZero animated:NO];
[self.collectionView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
or mark sure not any animation, and then call reloadData;
or
[self.collectionView performBatchUpdates:^{
//insert, delete, reload, or move operations
} completion:nil];