Multiple POST Requests Occurring - ios

I am seeing strange behaviour on an iOS app with a Rails/Heroku backend. I'll try to give as much detail as possible. Hopefully someone can point out a few possible areas where the problem could arise.
I wrote an iOS 7 app using XCode 5 where a user can log in and POST new articles and comments. I am using the AFNetworking library to communicate with my backend server (Rails 4, Heroku & Postgres). I am POSTing and GETing in the JSON format so I make use of AFJSONRequsetOperation to handle communication.
All my POST's use this method:
- (void) Post:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, id JSON))success
:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON))failure
// allocate a reachability object
Reachability* reach = [Reachability reachabilityWithHostname:#"www.google.com"];
reach.unreachableBlock = ^(Reachability*reach)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject];
[HUD flashMessage :#"Whoops" :#"No internet connection" :window];
});
failure(nil, nil, nil, nil);
};
reach.reachableBlock = ^(Reachability*reach)
{
// prepare base URL and calculate signature
// Something like: https://myapp.herokuapp.com
NSURL *url = [NSURL URLWithString:BasePath];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
[httpClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[httpClient setDefaultHeader:#"Accept" value:#"application/json"];
// Generates the POST URL
// Something like: https://myapp.herokuapp.com/api/v1/articles.json
NSString *basePath = [self GetPostURL];
NSLog(#"Post %# With parameters: %#", basePath, Parameters);
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:basePath parameters:Parameters];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:success failure:failure];
[operation start];
};
[reach startNotifier];
}
I did a bit of testing on my iPhone 5. Viewing the list of articles in a tableview, viewing article details in a detail view and then another tableview to view and add comments. I did a lot of testing and noticed no problem. Eventually I saw that there was 3 of the exact same comments. I thought I must of double-tapped on the 'Add' button so disable the button on-tap. I thought no-more about it as it was near impossible to re-create.
I have since updated my phone to iOS 8 and updated my XCode and SDKs to reflect this.
I put the app on my friends phone (iOS 8 & iPhone 5 C). She used it for a day without seeing any problems. She viewed articles, comments and added her own with no problems.
Today was a different story.
Firstly, a bit of background information. When you open the app - I refresh the list of articles so the app usually appears showing the old list of articles, then a loading icon appears, the tableview flashes and the newest articles appear.
When she opened the app today - it flashed multiple times, it looked like 10 or so. When the tableview appeared, the article she posted yesterday appeared twice. The exact article (title, description ,lat/lng, image) appeared twice but with different timestamps.
I thought the app was playing up on her phone so I checked the backend. It had two separate articles, different ids and timestamps but an exact copy otherwise.
Looking at the logs, there was something like 20/30 GET requests from her account. This seems to correspond with the flashing upon opening. It was updating the tableview multiple times.
Then the logs showed a POST request with all the params she POSTed the day before. Its as if she actually posted the exact same article today - but obviously she didn't.
Has anyone seen this kind of thing before?
The only things I can think of are:
The add article view controller was not destroyed after the initial post and somehow was made active again. I presumed ARC would handle all this?
Perhaps the AFNetworking library doesn't get flushed and thinks it needs to post again? Albeit nearly 24 hours later.
Is there a request batch operation on the library? Maybe this could be a problem?
I use a 'dispatch_get_global_queue' when posting. Is this okay?
On successfully adding an article, I move to the list again using the below code. Maybe there is a problem with it that I am unaware of?
ArticleListViewController *articleList = (ArticleListViewController *)[self.storyboard instantiateInitialViewController];
articleList.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self articleList animated:YES completion:nil];
I am at a loss trying to even replicate it, not to mention fix it.
I assume I am correct in pointing the finger at the app? It doesn't seem to be a problem with heroku/rails.
Any help would be greatly appreciated.
Update
I just had this issue again.
I did some testing yesterday, adding 3 articles. Everything worked as expected. I would occasionally go into the app every few hours to make sure everything is okay. I just opened the app now and it started flashing/flickering. It updated the list with a new article I posted yesterday. I did nothing with and it flickered again, and the same article was added again (so thats 2 duplicates with todays created_at date). I continued doing nothing and in a few seconds the other 2 articles that I added yesterday had been updated. One article was added 3 times, one article 2 times and one article was just duplicated once. This is bizarre behaviour.
Could there be some sort of global variable or something that keeps all the data? Maybe the controllers aren't removed completely?
Brian

After endless testing. I found the problem.
The duplicate POST(s) occurred at random. When it did occur it seemed to only happen 1% of the time, sometimes 5 minutes later, sometimes a day later.
I eventually figured out that it had something to do with switching from 3G to WiFi.
Adding an article on 3G, then changing to WiFi would cause a duplicate.
The problem is with the Reachability blocks in my above code.
I start the reachability notifier and set up two blocks for reachable and unreachable.
These blocks get hit when the phone changes reachability state. So moving from 3G to Wifi hit the reachable block again, resulting in a duplicate POST.
A simple fix is to call the following line at the start of each block to prevent further updates:
[reach stopNotifier];
Or, implement reachability like:
-(BOOL)reachable {
Reachability *r = [Reachability reachabilityWithHostName:#"www.google.com"];
NetworkStatus internetStatus = [r currentReachabilityStatus];
if(internetStatus == NotReachable) {
return NO;
}
return YES;
}
// In POST method
if([self reachable])
{
// Do POST
}else{
// No connection
}

Related

iPad Pro 3rd Gen Killing Foreground App Without Cause

I have an app that has been out in the wild for many years.
This app, in order to be 100% functional while offline, needs to download hundreds of thousands of images (1 for each object) one time only (delta updates are processed as needed).
The object data itself comes down without issue.
However, recently, our app has started crashing while downloading just the images, but only on newer iPads (3rd gen iPad Pros with plenty of storage).
The image download process uses NSURLSession download tasks inside an NSOperationQueue.
We were starting to see Energy Logs stating that CPU usage was too high, so we modified our parameters to add a break between each image, as well as between each batch of image, using
[NSThread sleepForTimeInterval:someTime];
This reduced our CPU usage from well above 95% (which, fair enough) to down below 18%!
Unfortunately, the app would still crash on newer iPads after only a couple of hours. However, on our 2016 iPad Pro 1st Gen, the app does not crash at all, even after 24 hours of downloading.
When pulling crash logs from the devices, all we see is that CPU usage was over 50% for more than 3 minutes. No other crash logs come up.
These devices are all plugged in to power, and have their lock time set to never in order to allow the iPad to remain awake and with our app in the foreground.
In an effort to solve this issue, we turned our performance way down, basically waiting 30 seconds in between each image, and 2 full minutes between each batch of images. This worked and the crashing stopped, however, this would take days to download all of our images.
We are trying to find a happy medium where the performance is reasonable, and the app does not crash.
However, what is haunting me, is that no matter the setting, and even at full-bore performance, the app never crashes on the older devices, it only crashes on the newer devices.
Conventional wisdom would suggest that should not be possible.
What am I missing here?
When I profile using Instruments, I see the app sitting at a comfortable 13% average while downloading, and there is a 20 second gap in between batches, so the iPad should have plenty of time to do any cleanup.
Anyone have any ideas? Feel free to request additional information, I'm not sure what else would be helpful.
EDIT 1: Downloader Code Below:
//Assume the following instance variables are set up:
self.operationQueue = NSOperationQueue to download the images.
self.urlSession = NSURLSession with ephemeralSessionConfiguration, 60 second timeoutIntervalForRequest
self.conditions = NSMutableArray to house the NSConditions used below.
self.countRemaining = NSUInteger which keeps track of how many images are left to be downloaded.
//Starts the downloading process by setting up the variables needed for downloading.
-(void)startDownloading
{
//If the operation queue doesn't exist, re-create it here.
if(!self.operationQueue)
{
self.operationQueue = [[NSOperationQueue alloc] init];
[self.operationQueue addObserver:self forKeyPath:KEY_PATH options:0 context:nil];
[self.operationQueue setName:QUEUE_NAME];
[self.operationQueue setMaxConcurrentOperationCount:2];
}
//If the session is nil, re-create it here.
if(!self.urlSession)
{
self.urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]
delegate:self
delegateQueue:nil];
}
if([self.countRemaining count] == 0)
{
[self performSelectorInBackground:#selector(startDownloadForNextBatch:) withObject:nil];
self.countRemaining = 1;
}
}
//Starts each batch. Called again on observance of the operation queue's task count being 0.
-(void)startDownloadForNextBatch:
{
[NSThread sleepForTimeInterval:20.0]; // 20 second gap between batches
self.countRemaining = //Go get the count remaining from the database.
if (countRemaining > 0)
{
NSArray *imageRecordsToDownload = //Go get the next batch of URLs for the images to download from the database.
[imageRecordsToDownload enumerateObjectsUsingBlock:^(NSDictionary *imageRecord,
NSUInteger index,
BOOL *stop)
{
NSInvocationOperation *invokeOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(downloadImageForRecord:)
object:imageRecord];
[self.operationQueue addOperation:invokeOp];
}];
}
}
//Performs one image download.
-(void)downloadImageForRecord:(NSDictionary *)imageRecord
{
NSCondition downloadCondition = [[NSCondition alloc] init];
[self.conditions addObject:downloadCondition];
[[self.urlSession downloadTaskWithURL:imageURL
completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error)
{
if(error)
{
//Record error below.
}
else
{
//Move the downloaded image to the correct directory.
NSError *moveError;
[[NSFileManager defaultManager] moveItemAtURL:location toURL:finalURL error:&moveError];
//Create a thumbnail version of the image for use in a search grid.
}
//Record the final outcome for this record by updating the database with either an error code, or the file path to where the image was saved.
//Sleep for some time to allow the CPU to rest.
[NSThread sleepForTimeInterval:0.05]; // 0.05 second gap between images.
//Finally, signal our condition.
[downloadCondition signal];
}]
resume];
[downloadCondition lock];
[downloadCondition wait];
[downloadCondition unlock];
}
//If the downloads need to be stopped, for whatever reason (i.e. the user logs out), this function is called to stop the process entirely:
-(void)stopDownloading
{
//Immediately suspend the queue.
[self.operationQueue setSuspended:YES];
//If any conditions remain, signal them, then remove them. This was added to avoid deadlock issues with the user logging out and then logging back in in rapid succession.
[self.conditions enumerateObjectsUsingBlock:^(NSCondition *condition,
NSUInteger idx,
BOOL * _Nonnull stop)
{
[condition signal];
}];
[self setConditions:nil];
[self setConditions:[NSMutableArray array]];
[self.urlSession invalidateAndCancel];
[self setImagesRemaining:0];
[self.operationQueue cancelAllOperations];
[self setOperationQueue:nil];
}
EDIT 2: CPU usage screenshot from Instruments. Peaks are ~50%, valleys are ~13% CPU usage.
EDIT 3: Running the app until failure in Console, observed memory issue
Alright! Finally observed the crash on my iPhone 11 Pro after over an hour downloading images, which matches the scenario reported by my other testers.
The Console reports my app was killed specifically for using too much memory. If I am reading this report correctly, my apps used over 2 GB of RAM. I'm assuming that this has to do more with the internal management of NSURLSESSIOND, since it is not showing this leak during debugging with either Xcode or Instruments.
Console reports: "kernel 232912.788 memorystatus: killing_specific_process pid 7075 [PharosSales] (per-process-limit 10) 2148353KB - memorystatus_available_pages: 38718"
Thankfully, I start receiving memory warnings around the 1 hour mark. I should be able to pause (suspend) my operation queue for some time (let's say 30 seconds) in order to let the system clear its memory.
Alternatively, I could call stop, with a gcd dispatch after call to start again.
What do you guys think about this solution? Is there a more elegant way to respond to memory warnings?
Where do you think this memory usage is coming from?
EDIT 4: Eureka!! Found internal Apple API memory leak
After digging I 'killing specific process' memory-related console message, I found the following post:
Stack Overflow NSData leak discussion
Based on this discussion surrounding using NSData writeToFile:error:, I looked around to see if I was somehow using this function.
Turns out, the logic that I was using to generate a thumbnail from the original image used this statement to write the generated thumbnail image to disk.
If I commented out this logic, the app no longer crashed at all (was able to pull down all of the images without failure!).
I had already planned on swapping this legacy Core Graphics code out for the WWDC 2018-demonstrated usage of ImageIO.
After recoding this function to use ImageIO, I am pleased to report that the app no longer crashes, and the thumbnail logic is super-optimized as well!
Thanks for all your help!

Why does an asynchronous NSURLConnection make the UI sluggish on iOS?

I noticed that the framerate while scrolling a collection view on an iPhone 4 dropped significantly (at times to 5 FPS) when a download using NSURLConnection was taking place in the background. I first suspected AFNetworking to be the culprit, but it turns out that the same thing happens when I simply use a block:
- (void)startBlockDownload:(id)sender
{
NSLog(#"starting block download");
dispatch_queue_t defQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
void (^downloadBlock) (void);
downloadBlock = ^{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:_urlString]];
NSURLResponse *response = nil;
NSError *error = nil;
NSData* result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(#"block request done");
};
dispatch_async(defQueue, downloadBlock);
}
What gives? Is a background download so demanding that it renders the UI extremely sluggish? Is it the slow flash memory? Is there anything that can be done to keep the UI very responsive while doing a background download?
I've created a sample project to demonstrate the issue: https://github.com/jfahrenkrug/AFNetworkingPerformanceTest
Also see this issue on AFNetworking that I have started about the topic: https://github.com/AFNetworking/AFNetworking/issues/1030#issuecomment-18563005
Any help is appreciated!
As the comments you linked to say: The iPhone 4 is still a single-core machine so no matter how much "multitasking" you do, it will still only be executing one set of code at once. What's worse is that the sendSynchronousRequest and the UI code are both blocking sets of code, so they will take up the maximum amount of time allotted to them by the OS and then the OS will perform an expensive context switch to prepare to execute the other.
You can try two things:
1) Use the async API of NSURLConnection instead of dispatching away a synch request (though I have the feeling you tried this with AFNetworking).
2) Lower the priority of the queue you dispatch to (preferably to DISPATCH_QUEUE_PRIORITY_BACKGROUND if possible). This might give the main queue more cycles to execute on, but it may not since the entire operation is just one big chunk.
If those both fail to work, then it probably simply is too much for a single-core processor to handle (scrolling is not a light operation, and downloading requires constant running time to receive data). That's my best guess anyway...

Stopping an NSOperationQueue

I have an NSOperationQueue that handles importing data from a web server on a loop. It accomplishes this with the following design.
NSURLConnect is wrapped in an NSOperation and added to the Queue
On successful completion of the download (using a block), the data from the request is wrapped in another NSOperation that adds the relevant data to Core Data. This operation is added to the queue.
On successful completion (using another block), (and after a specified delay) I call the method that started it all and return to step 1. Thus, i make another server call x seconds later.
This works great. I'm able to get data from the server and handle everything on the background. And because these are just NSOperations I'm able to put everything in the background, and perform multiple requests at a time. This works really well.
The ONLY problem that I currently have is that I'm unable to successfully cancel the operations once they are going.
I've tried something like the following :
- (void)flushQueue
{
self.isFlushingQueue = YES;
[self.operationQueue cancelAllOperations];
[self.operationQueue waitUntilAllOperationsAreFinished];
self.isFlushingQueue = NO;
NSLog(#"successfully flushed Queue");
}
where self.isFlushingQueue is a BOOL that I use to check before adding any new operations to the queue. This seems like it should work, but in fact it does not. Any ideas on stopping my Frankenstein creation?
Edit (Solved problem, but from a different perspective)
I'm still baffled about why exactly I was unable to cancel these operations (i'd be happy to keep trying possible solutions), but I had a moment of insight on how to solve this problem in a slightly different way. Instead of dealing at all with canceling operations, and waiting til queue is finished, I decided to just have a data structure (NSMutableDictionary) that had a list of all active connections. Something like this :
self.activeConnections = [NSMutableDictionary dictionaryWithDictionary:#{
#"UpdateContacts": #YES,
#"UpdateGroups" : #YES}];
And then before I add any operation to the queue, I simply ask if that particular call is On or Off. I've tested this, and I successfully have finite control over each individual server request that I want to be looping. To turn everything off I can just set all connections to #NO.
There are a couple downsides to this solution (Have to manually manage an additional data structure, and every operation has to start again to see if it's on or off before it terminates).
Edit -- In pursuit of a more accurate solution
I stripped out all code that isn't relevant (notice there is no error handling). I posted two methods. The first is an example of how the request NSOperation is created, and the second is the convenience method for generating the completion block.
Note the completion block generator is called by dozens of different requests similar to the first method.
- (void)updateContactsWithOptions:(NSDictionary*)options
{
//Hard coded for ease of understanding
NSString *contactsURL = #"api/url";
NSDictionary *params = #{#"sortBy" : #"LastName"};
NSMutableURLRequest *request = [self createRequestUsingURLString:contactsURL andParameters:params];
ConnectionCompleteBlock processBlock = [self blockForImportingDataToEntity:#"Contact"
usingSelector:#selector(updateContactsWithOptions:)
withOptions:options andParsingSelector:#selector(requestUsesRowsFromData:)];
BBYConnectionOperation *op = [[BBYConnectionOperation alloc] initWithURLRequest:request
andDelegate:self
andCompletionBlock:processBlock];
//This used to check using self.isFlushingQueue
if ([[self.activeConnections objectForKey:#"UpdateContacts"] isEqualToNumber:#YES]){
[self.operationQueue addOperation:op];
}
}
- (ConnectionCompleteBlock) blockForImportingDataToEntity:(NSString*)entityName usingSelector:(SEL)loopSelector withOptions:(NSDictionary*)options andParsingSelector:(SEL)parseSelector
{
return ^(BOOL success, NSData *connectionData, NSError *error){
//Pull out variables from options
BOOL doesLoop = [[options valueForKey:#"doesLoop"] boolValue];
NSTimeInterval timeInterval = [[options valueForKey:#"interval"] integerValue];
//Data processed before importing to core data
NSData *dataToImport = [self performSelector:parseSelector withObject:connectionData];
BBYImportToCoreDataOperation *importOperation = [[BBYImportToCoreDataOperation alloc] initWithData:dataToImport
andContext:self.managedObjectContext
andNameOfEntityToImport:entityName];
[importOperation setCompletionBlock:^ (BOOL success, NSError *error){
if(success){
NSLog(#"Import %#s was successful",entityName);
if(doesLoop == YES){
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:loopSelector withObject:options afterDelay:timeInterval];
});
}
}
}];
[self.operationQueue addOperation:importOperation];
};
}
Cancellation of an NSOperation is just a request, a flag that is set in NSOperation. It's up to your NSOperation subclass to actually action that request and cancel it's work. You then need to ensure you have set the correct flags for isExecuting and isFinished etc. You will also need to do this in a KVO compliant manner. Only once these flags are set is the operation finished.
There is an example in the documentation Concurrency Programming Guide -> Configuring Operations for Concurrent Execution. Although I understand that this example may not correctly account for all multi-threaded edge cases. Another more complex example is provided in the sample code LinkedImageFetcher : QRunLoopOperation
If you think you are responding to the cancellation request correctly then you really need to post your NSOperation subclass code to examine the problem any further.
Instead of using your own flag for when it is ok to add more operations, you could try the
- (void)setSuspended:(BOOL)suspend
method on NSOperationQueue? And before adding a new operation, check if the queue is suspended with isSuspended?

IOS how to wait until `didReceiveData` handler is called

Im developing ios application which is getting data from the web server. I want everything else to wait, until one of the handlers of this class is called and completed. I know it is possible by using dispatch/threads, but i just can't figure out how.
-(void)callWebService:(NSString*)URL:(NSString*)SOAP{
NSURL *url = [NSURL URLWithString:URL];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:#"POST"];
[req setHTTPBody:[SOAP dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *con = [[NSURLConnection alloc] initWithRequest:req delegate:self];
if(con){
[con start];
}
}
and at the end of this method continues code outside this class. but i want to wait until this handler is called (and completed):
-(void)connection:(NSURLConnection *)c didReceiveData:(NSData *)data{
NSString *res = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#",res);
Ukol_Parser *parser = [Ukol_Parser alloc];
[parser parseUkol:res];
}
because the parser here puts data into sqlite db and outside this class data are being read. But the "outside" code is being executed faster than i get response and handler is called....
If you want "everything else to wait", then it sounds like what you really want to do are synchronous requests.
Check out [NSURLConnection sendSynchronousRequest:returningResponse:error:]
However, make sure to do this thing on a background thread because if you do it on the main thread, your UI will block and your app will look unresponsive to user touches or anything else.
I'm nervous that you accepted the answer regarding sendSynchronousRequest from the background queue because, from a practical perspective, this is no different than your didReceiveData-based implementation. Specifically, if you do perform synchronous request from a background queue, it will not make "everything else wait".
But if you neglect to do this synchronous request from the background queue (i.e. if you do it from the main thread), you end up with a horrible UX (the app is frozen and the user is wondering whether the app has crashed), and worse, your app could be killed by the iOS "watchdog process" if it takes too long.
With all deference to the various answers, sending a synchronous request on a background queue is indistinguishable from the existing NSURLConnectionDataDelegate-based approach. What you really need to do is accept the fact that the rest of the app will not freeze, and therefore simply update the UI to let the user know what's happening, namely that (a) provide some visual cue that the app is not dead; and (b) prevent the user from interacting with your existing UI.
For example, before issuing your network request, add a view that will cover/dim the rest of your UI, prevent user interaction with the existing UI, and add a spinner to let the user know that the app is busy doing something. So, define a class property for a UIView that will dim the rest of the UI:
#property (nonatomic, strong) UIView *dimView;
And then, before the network request, update the UI:
// add a view that dims the rest of the UI, so the user has some visual cue that they can't use the UI yet
// by covering the whole UI, you're effectively locking the user out of using the app
// until the network request is done
self.dimView = [[UIView alloc] initWithFrame:self.view.bounds];
self.dimView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
self.dimView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:self.dimView];
// add a spinner that shows the user that something is really happening
UIActivityIndicatorView *indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicatorView.center = self.dimView.center;
indicatorView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
[indicatorView startAnimating];
[self.dimView addSubview:indicatorView];
[self callWebService:URL withSOAP:SOAP];
And then, in the connectionDidFinishLoading method (or if you used sendSynchronousRequest from a background queue, in the latter portion of code to dispatched to that background queue), after you finish parsing your data, you want to:
[self.dimView removeFromSuperview];
self.dimView = nil;
// now do what ever to need do to update your UI, e.g.:
//
// [self.tableView reloadData]
But the key is that you absolutely do not want to issue a synchronous network request from the main queue. You should (a) do your network request asynchronously (either synchronously on a background queue, or as you originally implemented) so that the iOS watchdog process doesn't kill your app; (b) let the user know the app is making some network request and hasn't frozen on them (e.g. a UIActivityIndicatorView; and (c) when the request is done, remove these "the app is doing network request" UI elements and refresh the rest of the UI now that your network request is done.
Finally, when testing your app, make sure you test it in real-world networking situations. I'd suggest you install the Hardware IO tools (available in Xcode menu, under "Open Developer Tools" - "More Developer Tools") and check out the Network Link Conditioner. This lets you simulate real-world network situations (e.g. a bad 3G or Edge network condition) on the iOS simulator. We get lulled into a false sense of performance when we test our apps in typical development environments with ideal network connectivity. Devices "in the wild" suffer a wide range of degraded network situations, and it's good to test your app in a similar, suboptimal network situation.
kind of a wild solution, but this actually worked https://gist.github.com/62940 :D
use synchronous calls . but It'd change the design of your class because a synchronous call will block and leave the app hanging
Post a notification from your didReceiveData: method, and have your other class observe that notification (or you could use a delegate setup, if it's easy to get a reference to this class from the other so you can set your other class as the delegate of this one). In the notification's selector method, start executing the rest of your code.

IOS Iphone app randomly hangs indefinitely

App being built on Xcode 4, for a base IOS level of 4 and above, tested on a variety of IPhones. Info in app is loaded from a JSON feed.
In my attempts to track down what's happening I've repeatedly debugged through this app, a fairly basic one that has a few views, overall nothing amazing or new in it as far as apps go.
The problem manifests itself occasionally, a lot of the time in our testing it has been if we've turned off Wifi to test on a cellular connection the app will hang when loading.
What happens is that the splash screen appears and the white spinning activity wheel spins as if it's loading the ad (an ad is displayed as part of the overall loading process) but it just sits there spinning.
So, most obvious question, is the ad the problem? Is it large or loading slowly? No, the JSOn feed is speedy (we check every time the app hangs and inbetween times as well to make sure it's all loading ok) and the ad is about 20k.
Debugging gets as far as the didFinishLaunchingWithOptions method which I'll list below, it reaches the end of that method and the debugger essentially reports nothing else. It just sits there, nothing visible in the XCode debug navigator, no break points hit.
If step through that method, once it gets to the end it goes about 6-7 steps into compiled code then simply does the same thing.
Here's the code from that method:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions {
[self loadData];
splashViewController = [[OurSplashViewController alloc] initWithNibName:nil bundle:nil];
splashViewController.delegate = self;
OurWebViewController *webViewController = (OurWebViewController *)[[[tabBarController viewControllers] objectAtIndex:2] topViewController];
webViewController.url = [NSURL URLWithString:#"http://m.testURL.com/this_app/"];
webViewController.path = #"/MyPath/Sample";
[window addSubview:splashViewController.view];
[window makeKeyAndVisible];
return YES;
}
So, it gets to the 'YES' of that method and then the debugger sails off into the land of ones and zeroes, reporting nothing back and hanging the app.
Has anyone got any idea what could be causing this?
UPDATE:
Seem to have isolated that the hanging comes down to a JSON call that is sent but no response is received. So my code should handle that after a while.
Here's the JSON code, maybe someone can notice something I'm missing in there:
- (void)fetchWithURL:(NSURL *)URL {
if (!URL) {
return;
}
self.jsonURL = URL;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.jsonURL];
if (timeout) {
[request setTimeoutInterval:timeout];
}
[request addValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
connection_ = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
[request release];
if (connection_ != nil) {
if ([self.delegate respondsToSelector:#selector(JSONFetchWillBeginDownload:)]) {
[self.delegate JSONFetchWillBeginDownload:self];
}
receivedData_ = [[NSMutableData alloc] initWithCapacity:0];
} else {
if ([self.delegate respondsToSelector:#selector(JSONFetch:downloadDidFailWithError:)]) {
[self.delegate JSONFetch:self downloadDidFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:nil]];
}
}
}
According to Charles the reason for the failed response was:
Client closed connection before receiving entire response
I wonder is my timeout period too short?
If the spinning wheel (which is presumably part of the splash screen) is appearing, then the method you've posted is obviously working. If the splash screen is not disappearing, the obvious place to start debugging is in the code that sends the request and the code that receives the response then dismisses the splash screen.
Use a proxy like Charles to verify that the request is going out and a response is coming back. If that is the case, debug the code that receives the response and dismisses the splash screen. If that isn't the case, debug the code that sends the request.
If you don't get anywhere, I suggest posting the relevant code.
Use Charles Proxy for debugging the data exchange with your remote services and the ad-server.
See this tutorial for setting everything up.
Looks like my timeout period was too short for the ads and as a result if the user was off WiFi then the JSON took a little too long to load, causing the issue.
Fixed it by upping the timeout to a normal level.

Resources