UITableView - AFNetworking operation in background freezes scrolling - ios

I have a sample application with a UITableViewController.
As in the facebook newsfeed, the app is supposed to download a first time X news, and then fetch news progressively as the user scroll.
Here is my implementation :
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if (indexPath.row == self.newsList.count-PADDLE_BEFORE_FETCHING && !cantFetchMore)
if (!fetching){
fetching = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[self fetchNews];
});
}
}
(the idea is to start fetching additional news when we reach the N-PADDLE_BEFORE_FETCHING cell, only if we can still fetch some - see below- and if fetching is still not currently running)
and then the implementation of fetchNews :
-(void)fetchNews{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *url = [NSString stringWithFormat:#"%#%#%#%#%d%#",HOSTNAME,GET_NEWS,[defaults objectForKey:#"oAuthToken"],#"&limit=",FETCH_SIZE_NEWS,[NSString stringWithFormat:#"&offset=%d",self.newsList.count]];
NSURLRequest *request =[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
#if DEVELOPMENT_MODE
NSLog(#"News : %#",JSON);
NSLog(#"Response : %#\n Request : %#",response,request);
#endif
//NSLog(#"Number of news fetched : %d",((NSArray*)JSON[#"data"]).count);
for (NSDictionary *d in JSON[#"data"]){
News *new = [[News alloc] initWithDictionary:d];
[self.newsList addObject:new];
new = nil;
}
if ((((NSArray*)JSON[#"data"]).count)%FETCH_SIZE_NEWS !=0) cantFetchMore = YES;
//NSLog(#"%d cantFetch",cantFetchMore);
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self.tableView reloadData];
fetching = NO;
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request error : %# %# %#",request,error, JSON);
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
fetching = NO;
}];
[operation start];
}
This will fetch FETCH_SIZE_NEWS additional news from the server starting at the good offset which is the current size of the newsList array.
Also, if the count of fetched news % FETCH_SIZE_NEWS is different from 0, that means that we cannot fetch additional news (which will prevent from calling the webservice while scrolling the UITableView).
My issue is that when the fetching is done (exactly when I see the activity wheel running in the status bar), it blocks the GUI, and I cant continue to scroll down from the n-PADDLE_BEFORE_FETCHING cells to the n cells, or even scroll up to the previously loaded cells.
I don't really understand why as AFNetworking is supposed to run asynchronously.
Any ideas?
Thanks,

The for-loop in the completion block is running on the main thread and may be causing the slow-down. Try sending that code to another thread/queue.

As stated above buy Guy Kogus, the processing within the complete block of the AFNetworking operation was adding a bit a freezer on the main thread while scrolling.
Just added
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{//processing block}
Within the complete block (notably the foreach loop) and this is far better.
Thanks

Related

iOS app crash recover method

Since 2 yrs I have been trying different ways to find the solution of app crash while click back button.
My application scenario:
In a tableview contoller I have to load list of users, On view did load I call getData(Asyncronous download) API method to load data. At the time of data download, If user press back button my application gets crash due to null value objects. That says all of my variable memory deallocated.
To overcome this problem, I used some loading indicator which lock UIScreen untill data download.
Questions:
Is there any alternatives to prevent crash, UIScreen Lock
Other applications use Activity Indicator in Menu bar without UIScreen Lock. How they are doing?
Need help to recover this issue
Here is my sample code to download data :
Below code doesnt crash app. But it download data even I cancel operations on dealloc
viewDidLoad:
ShowNetworkActivityIndicator();
_processQueue = [[NSOperationQueue alloc] init];
_processQueue.maxConcurrentOperationCount = 4;
_processQueue.name = #"Events Processing";
[self loadData];
loadData:
-(void)loadData
{
[_processQueue addOperationWithBlock: ^ {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:[[NSURL alloc] initWithString:#"https://restcountries.eu/rest/v1/all"]];
NSURLResponse *response;
NSData *urlData=[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
NSString *data=[[NSString alloc]initWithData:urlData encoding:NSUTF8StringEncoding];
NSDictionary *search = [NSJSONSerialization JSONObjectWithData:[data dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
[[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
_countryListArray=[search mutableCopy];
[self.tableViewSample reloadData];
HideNetworkActivityIndicator();
}];
}];
}
I tried cancelAllOperations in dealloc:
[_processQueue setSuspended:YES];
[_processQueue cancelAllOperations];
Can you try inserting the reload data is dispatch_async(dispatch_get_main(),void (^){}); callback , main thread , I think the reload happening in the background thread is crashing the app.
[[NSOperationQueue mainQueue] addOperationWithBlock: ^ { _countryListArray=[search mutableCopy]; dispatch_async(dispatch_get_main_queue(), ^{
[self.tableViewSample reloadData];
HideNetworkActivityIndicator();
});}];

Sending NSOperationQueue to UITableView as a DataSource

i have written code to downloading data from server using NSOperationQueue and NSOperation. and now i want to show progress on UserInterface. i used UITableView and used NSOpeartionQueue as a datasource in tableview delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[[Downloadmanager sharedInstance] downloadOperationQueue] count];
}
and bind NSOperation`s properties to UITableViewCell.
1) Is this a fisible solution to sending NSOperationQueue as a datasource to tableview delegate ?
2) How to implement notification to reload tableview when NSOperation's state changes?
Thanks.
I don't think it's the proper way of showing progress using NSOperationQueue as a datasource to tableview. You can use networking library like AFNetworking for downloading data and use setDownloadProgressBlock: method for showing progress. Refer this link for the code download progress.
It's easy to reload tableview when the download completes, just call [tableView reloadData] in completionblock.
Here is the code which shows image downloading using AFNetworking which you can easily change for data download.(refer this gist)
- (void)downloadMultiAFN {
// Basic Activity Indicator to indicate download
UIActivityIndicatorView *loading = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[loading startAnimating];
[self.imageView.superview addSubview:loading];
loading.center = self.imageView.center;
// Create a request from the url, make an AFImageRequestOperation initialized with that request
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.picUrl]];
AFImageRequestOperation *op = [[AFImageRequestOperation alloc] initWithRequest:request];
// Set a download progress block for the operation
[op setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = (float) totalBytesRead/totalBytesExpectedToRead;
} else self.progressBar2.progress = (float) totalBytesRead/totalBytesExpectedToRead;
}];
// Set a completion block for the operation
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
self.imageView.image = responseObject;
self.image = responseObject;
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = 0;
} else self.progressBar2.progress = 0;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {}];
// Start the image download operation
[op start];
// Remove the activity indicator
[loading stopAnimating];
[loading removeFromSuperview];
}
That is an interesting idea, but I don't think it's a good practice make such a "high coupling" - linking model so tightly to the view.
I'd approach it as - download the data on the background thread as you already do - with NSOperationQueue but save it to some kind of an object; say NSMutableArray that serves as the data source for the table view.
Every time a single operation ends (use completion handlers or KVO to get informed) - update the table view. The update can be done two ways - reloading or updating. I'll leave the choice up to you - you can read further discussion about that in this question.

How to load huge data from the server ,storing it in local database and displaying in UItableView

I am downloading huge data using AFnetworking and storing into the local database and displaying in the tableview, When i download less data , table loads soon but when i download huge data and store its getting stuck for sometime because all operations are on main thread.
Can somebody tell me how to do this all process on background , so that user can use the App without any problem .
Code:
-(void) AddRecentCallLogs:(NSDictionary *)args :(NSString * ) type{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([type isEqualToString:#"dailed"])
{
NSString *phoneNumber=[args objectForKey:#"destination_number"];;
NSString *date=[args objectForKey:#"added_date_time"];
NSString *name=[self getName:phoneNumber];
if(name==NULL)
{
name=#"NOT_FOUND";
}
appDelegate=(AppDelegate *)[[UIApplication sharedApplication]delegate];
[appDelegate SetPhoneLogs:name :phoneNumber :date :#"Dailed"];
[self.tableView reloadData];
}
else
{
NSString *phoneNumber=[args objectForKey:#"source_number"];;
NSString *date=[args objectForKey:#"added_date_time"];
NSString *name=[self getName:phoneNumber];
if(name==NULL)
{
name=#"NOT_FOUND";
}
appDelegate=(AppDelegate *)[[UIApplication sharedApplication]delegate];
[appDelegate SetPhoneLogs:name :phoneNumber :date :#"Received"];
[self.tableView reloadData];
}
});
}
In other function I am calling above method:
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:request1 success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
resp = [JSON objectForKey:#"result"];
//NSLog(#"%#-- %#",JSON,resp);
self.recent_calllogs = [JSON objectForKey:#"log"];
if(recent_calllogs.count<=0)
[SVProgressHUD dismiss];
for(NSDictionary *info in recent_calllogs)
{
[self AddRecentCallLogs:info :#"received"];
}
NSString *last_record=[JSON objectForKey:#"lastid"];
if(last_record!=NULL)
{
[[NSUserDefaults standardUserDefaults]
setObject:last_record forKey:#"recent-R"];
// NSString *savedValue = [[NSUserDefaults standardUserDefaults]stringForKey:#"recent-R"];
//NSLog(#"saved:%#",savedValue);
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
implement paginator on server, and on device Pull-to-refresh, take a look at https://github.com/instructure/CKRefreshControl
loading large data at once it's too time consuming, 2 reasons for that :
downloading data and parsing it
saving it to database (for my experience 8MB of json text (30k objects) is saving for about 30-50 secs )
(optional) if you are using dynamic height for table row in table view, it is also very time consuming , because system calls method for calculating height for every row BEFORE rendering the table.

AFNetworking, callback when finished request?

I wonder if it's possible to get a callback when AFNetworkingfinished a request? I'm making an app in which I have a edge case, all downloaded items is checked is_deleted (and should not be displayed in the UI). If this is case I would like to automatically try to download older data (create a new AFNetworking request).
I have tried to call my loadData method again, from itself. But it creates an infinitive loop. Like this:
- (void)loadDataIsOlder:(NSNumber *)older
{
NSURLRequest *request = [NSURLRequest requestWithURL:self.streamUrl];
AFJSONRequestOperation *operation;
operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id jsonObject) {
NSDictionary *data = [jsonObject objectForKey:#"data"];
NSDictionary *meta = [jsonObject objectForKey:#"meta"];
[...]
for (NSNumber *deleted in (NSArray *)[data valueForKey:#"is_deleted"]) {
if ([deleted boolValue] == YES) {
self.shouldTryToLoadMore = YES;
} else {
self.shouldTryToLoadMore = NO;
}
}
if (self.shouldTryToLoadMore == YES) {
[self loadDataIsOlder:[NSNumber numberWithBool:YES]];
}
} [...]
}
So, I wonder if it's some other / better way to do what I want.
Subquestion
The array (NSArray *)[data valueForKey:#"is_deleted"] consists of BOOL int values. I wonder if its possible to easily calculate the values of an array containing ints, e.g. [1, 1, 1, 0] = 3? I want to check if all values in the array is 1:s.
I am not familiar with AFNetworking
I wonder if it's possible to get a callback when AFNetworking finished a request?
You could always set the completion block of the operation (will be called even on failure, on the thread the operation run in so be careful with UI updates).
like so: operation.completionBlock = ^{/*what ever you like to accomplish*/};
all downloaded items is checked is_deleted
I'm spotting a little problem in your algorithm.
You are not accumulating the deletion keys properly (you only respond to the last item status).
try: self.shouldTryToLoadMore &= [deleted boolValue]; instead of your if statement.
If you like something shorted (to avoid the "visible" loop) try:
NSMutableSet* set = [NSMutableSet setWithArray:[data valueForKey:#"is_deleted"]];
[set addObject:#YES];
if ([set count] == 1) {
//You have only deleted items in your request
}
note
You are capturing self in the success block, which might be problematic (retain cycle if you own the operation in self).
You can set a retry counter and put it in the failure block, so it would look something like this:
- (void)loadDataIsOlder:(NSNumber *)older
{
if (shouldTrytoReloadMore){
NSURLRequest *request = [NSURLRequest requestWithURL:self.streamUrl];
AFJSONRequestOperation *operation;
operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id jsonObject) {
NSDictionary *data = [jsonObject objectForKey:#"data"];
NSDictionary *meta = [jsonObject objectForKey:#"meta"];
[...]
for (NSNumber *deleted in (NSArray *)[data valueForKey:#"is_deleted"]) {
if ([deleted boolValue] == YES) {
self.shouldTryToLoadMore = YES;
[self loadDataIsOlder:[NSNumber numberWithBool:YES]];
} else {
self.shouldTryToLoadMore = NO;
}
}
}
} [...]
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
}
WHen you call the method the first time, set n as the number of times you need to retry.

Unresponsive TableView while WebRequest (AFNetworking) on device, but ok in simulator + MagicalRecord issue

I have an application that retrieves json (employees workschedules) from a web service using AFNetworking and displays them in a table view.
I have my webservice class that takes care of doing the request and once it is done, it stores these data into coredata (I have an another issue here, being that I use magicalRecord and the data does not persist, and I don't understand why) and then calls back its delegate (my tableViewController) telling it it's done, so this can load the workschedules into the cells.
WebServiceClient.m
NSURL *url = [NSURL URLWithString:stringUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
NSArray *workSchedules = [[[NSSet alloc] initWithArray:JSON] allObjects];
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
Workschedule *workscheduleEntity = nil;
NSError *error = nil;
for (NSDictionary *web_workschedule in workSchedules)
{//Inside this method I create other entities that will hydrate my workschedule entity, and it is done using the MR_CreateInContext
workscheduleEntity = [Workschedule workScheduleFromJSONDictionary:web_workschedule withError:&error];
[context MR_save];
}
if([self.delegate respondsToSelector:#selector(workSchedules)]){
[self.delegate workSchedules];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
LOG_ERROR(2,#"Received an HTTTP %d:", response.statusCode);
LOG_ERROR(2,#"The error was: %#", error);
if([self.delegate respondsToSelector:#selector(workSchedules:)]){
[self.delegate workSchedules:nil];//return error
}}];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:operation];
}
PendingWorkscheduleViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
[self.webServiceClient getMockedWorkSchedulesForEmployee:[NSNumber numberWithInt:1]];
[self workSchedules];
}
-(void)workSchedules
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pending == YES"];
NSArray *pendingWorkSchedules = [Workschedule MR_findAllWithPredicate:predicate];
self.pendingWorkSchedules = pendingWorkSchedules;
[self.tableView reloadData];
}
My problem is that when i run this while the request is processed the UI is unresponsive (it's a very brief time, but if the request were to increase...) so that if i load the table view and right away try to scroll or click the back button, it just ignores it as it is "frozen". This behavior is on my iphone 4s. On the simulator this works fine and I can't wrap my head around why is that. I tried to call the "[self.webServiceClient getMockedWorkSchedulesForEmployee:[NSNumber numberWithInt:1]];" in a queue using GCD, I tried using performSelectorInBackground: WithObject: etc but still the same (even though with this last method it seemed a little more efficient, but it's an impression and only on the simulator, no changes on the device).
As far as magicalRecord goes I will make separate question.
I would appreciate your help.
Fixed it. The problem is that the success block run on the main thread! (which I did not understand). I just used GCD in the success block with a background queue for processing the data and the main queue to store this data in core data.
As far as magical record issue, i needed to save "nestedContext".
Cheers everyone.

Resources