I have an NSURLConnection in a tableview cell subclass that can download most files. I noticed, however, that some fail to start downloading, and time out. An example would be this URL, which is just a test zip file that downloads fine in any other browser. Heres my code for the download
-(void)downloadFileAtURL:(NSURL *)url{
self.downloadedData = [[NSMutableData alloc] init];
self.url = url;
conn = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:self.url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:1200.0] delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)response
{
int statusCode = [response statusCode];
if (statusCode == 200){
self.fileName.text = response.URL.lastPathComponent;
self.respo = response;
expectedLength = [response expectedContentLength];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[self.downloadedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
CFStringRef mimeType = (__bridge CFStringRef)[_respo MIMEType];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
NSString *fileName = [NSString stringWithFormat:#"%#.%#", [[_respo suggestedFilename] stringByDeletingPathExtension], (__bridge NSString *)extension];
[[NSFileManager defaultManager] createFileAtPath:[[self docsDir] stringByAppendingPathComponent:[NSString stringWithFormat:#"Downloads/%#", fileName]] contents:_downloadedData attributes:nil];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(#"Download failed with error: %#", error);
}
Anybody see anything that might cause this?
Heres the error:
Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo=0x1fd2c650
{NSErrorFailingURLStringKey=http://download.thinkbroadband.com/10MB.zip,
NSErrorFailingURLKey=http://download.thinkbroadband.com/10MB.zip,
NSLocalizedDescription=The request timed out., NSUnderlyingError=0x1fdc90b0 "The request timed out."}
"I have an NSURLConnection in a tableview cell subclass " - never do this. As Sung-Pil Lim already pointed out correctly, TableView Cells will be reused which may cause this issue.
Anyway, the response data of your connection is a property of the model. The model might encapsulate how it gets to this data. If that data is not immediately available once it will be accessed, it should provide a "placeholder" value instead and start an asynchronous task which retrieves this data.
Suppose a model's property, an image, will be accessed by the view controller in order to be displayed by a view. The model has not yet loaded its actual image - and thus it returns a "placeholder image" in order to let the view display something. But at the same time the model is starting an asynchronous task to load the image. When this connection is finished loading with the data, the model updates internally its property - thereby replacing the placeholder with the real image. The update of the property should be performed on the main thread - since the UIKit views may access the same property as well.
During initialization, the View Controller has registered as an observer of the model's property (see KVO). When the model's property is updated, the controller gets notified. The View Controller then performs appropriate actions so that the view will be redrawn and displays the new updated value.
Your model should have a "cancel" method, which will be send to the model from the controller when the actual value of the model's property is not required anymore. For example, the user switched to another view (see viewWillDisappear).
I tried your codes.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[self.downloadedData appendData:data];
NSLog(#"%d", data.length);
}
2013-05-04 01:51:13.811 SomethingTodo[2732:c07] 1124
2013-05-04 01:51:13.856 SomethingTodo[2732:c07] 1448
2013-05-04 01:51:14.075 SomethingTodo[2732:c07] 1448
2013-05-04 01:51:17.180 SomethingTodo[2732:c07] 1448
2013-05-04 01:51:17.295 SomethingTodo[2732:c07] 1448
It's working... on ViewController
'request timeout error' was brought to network connection. or...
Are you resuing UITableViewCell? If you initialize for cell reuse codes deal with connection. maybe bring to trouble. Just i thought.
If you attach more your codes. Could I help you more then this.
I would start with a clean slate and just use basic code to work the download. Load in lots of NSLog(s) to track everything. If that works, keep adding your custom code and see if you stumble across an error. I suggest basic NSURLConnection code:
-(void)startDownloading:(NSString *)URLaddress{
NSLog(#"start downloading from: %#",URLaddress);
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:[URLaddress stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
__unused NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(#"didReceiveResponse: %#", response);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSLog(#"didReceiveData");
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(#"Connection failed! Error - %# %#",[error localizedDescription], [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(#"connectionDidFinishLoading");
}
try with HCDownloadViewController and you can check which url is not downloaded. and next time sync for that particular url which is not downloaded.
.h file
#import "HCDownloadViewController.h"
#interface HomeViewController_iPhone : UIViewController<HCDownloadViewControllerDelegate>
{
HCDownloadViewController *tblDownloadHairStyle;
}
#property (nonatomic,retain) HCDownloadViewController *tblDownloadHairStyle;
.m file
#define kAppDirectoryPath NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)
#synthesize tblDownloadHairStyle
- (void)viewDidLoad
{
[super viewDidLoad];
tblDownloadHairStyle=[[HCDownloadViewController alloc] init];
tblDownloadHairStyle.delegate=self;
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)response
{
[self createDocumentDirectory:#"Downloaded_HairStyle"];
NSString *pathHair=[self getDocumentDirectoryPath:#"Downloaded_HairStyle"];
tblDownloadHairStyle.downloadDirectory = pathHair;
////You can put url in for loop, it create queue for downloading.
[tblDownloadHairStyle downloadURL:[NSURL URLWithString:#"yourUrl"] userInfo:YourResponseDictonary];
}
-(void)createDocumentDirectory:(NSString*)pStrDirectoryName
{
NSString *dataPath = [self getDocumentDirectoryPath:pStrDirectoryName];
if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath])
[[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:NO attributes:nil error:NULL];
}
-(NSString*)getDocumentDirectoryPath:(NSString*)pStrPathName
{
NSString *strPath = #"";
if(pStrPathName)
strPath = [[kAppDirectoryPath objectAtIndex:0] stringByAppendingPathComponent:pStrPathName];
return strPath;
}
#pragma mark-
#pragma mark-HCDownloadViewController Delegate Method
- (void)downloadController:(HCDownloadViewController *)vc startedDownloadingURL:(NSURL *)url userInfo:(NSDictionary *)userInfo {
}
- (void)downloadController:(HCDownloadViewController *)vc finishedDownloadingURL:(NSURL *)url toFile:(NSString *)fileName userInfo:(NSDictionary *)userInfo {
if (vc==tblDownloadHairStyle) {
if ([tblDownloadHairStyle numberOfDownloads]==0) {
NSLog(#"AllDownLoad are complete");
}
}
}
- (void)downloadController:(HCDownloadViewController *)vc failedDownloadingURL:(NSURL *)url withError:(NSError *)error userInfo:(NSDictionary *)userInfo {
NSLog(#"failedDownloadingURL=%#",url);
}
https://github.com/H2CO3/HCDownload
accept any response with http response code range 200-299 and disable caching on the http-connector.
double check your url address conforms to RFC 2396. so it must include HTTP://
Do you have any libraries (TestFlight, UA, etc) in the project? Try removing them and re-test. We had an app that used NSUrlConnection with TestFlight SDK that caused all sorts of sporadic network problems.
NSURLConnection timing out
ASIHTTPRequest request times out
https://github.com/AFNetworking/AFNetworking/issues/307
Related
I am trying to retrieve a Facebook profile picture, however I am having trouble being able to check when the image has been downloaded?
First I create a variable.
#property (strong, nonatomic) NSMutableData *imageData;
Than I start the connection.
-(void)getUserPicture {
//Grab user profile picture
imageData = [[NSMutableData alloc] init]; // the image will be loaded in here
NSString *urlString = [NSString stringWithFormat:#"http://graph.facebook.com/%#/picture?type=large", userId];
NSMutableURLRequest *urlRequest =
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
NSURLConnection *urlConnection = [[NSURLConnection alloc] initWithRequest:urlRequest
delegate:self];
if (!urlConnection) NSLog(#"Failed to download picture");
}
After that I try to check when it is done so I can upload the file to my backend, however my problem is connectionDidFinishLoading calls almost instantly before the image has downloaded.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
imageData = [NSMutableData data];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[imageData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
userPicture = [UIImage imageWithData:imageData];
NSLog(#"%#",userPicture); //this returns null :(
}
The weird thing is if I call this method twice, the NSLog doesn't return null, it actually returns the photo. So why is connectionDidFinishedLoading calling before the image has downloaded from Facebook?
The problem is almost certainly neither NSURLConnection nor the Facebook API, but rather how you're calling it. But, your question doesn't include enough information for us to diagnose it.
So, first, expand your methods to include more diagnostic information, for example:
// check the response header when we receive the response
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
imageData = [NSMutableData data];
// if `statusCode` is not 200, show us what it was ...
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
int statusCode = [(NSHTTPURLResponse *)response statusCode];
if (statusCode != 200) {
NSLog(#"Status code was %ld, but should be 200.", (long)statusCode);
NSLog(#"response = %#", response);
}
}
}
// make sure to detect and report any errors
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"didFailWithError: %#", error);
}
// when you're done, if we fail to create `UIImage`, then it obviously
// wasn't an image, so let's see what it was.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
userPicture = [UIImage imageWithData:imageData];
// if not an image, then ...
if (!userPicture) {
NSString *responseString = [[NSString alloc] initWithData:imageData encoding:NSUTF8StringEncoding];
if (responseString) {
// if it was a string, show it to us ...
NSLog(#"did not receive image: responseString = %#", responseString);
} else {
// if neither a string nor image data, then what was it ...
NSLog(#"did not receive image: imageData = %#", imageData);
}
}
// By the way, I'd expect to see you do something here to update the UI with the image;
// all of these delegate methods were called asynchronously, so you have
// to do something here that triggers the update of the UI, e.g.
//
// self.imageView.image = userPicture
}
By the way, I typed the above without the benefit of Xcode's syntax checking and the like, so don't be surprised if there are some errors there. But worry less about the actual code and focus on the the three diagnostic pillars this illustrates: 1. Look at the response headers and make sure they're ok, not reporting some non-200 status code; 2. Implement delegate that will report networking errors; and 3. If image conversion failed, then you obviously didn't receive an image, so stop and figure out what you actually received. (Often if the server had trouble fulfilling your request, the response is actually HTML or something like that which tells you why it had problems. If you don't look at it, you're flying blind.)
Second, you can watch the network connection by using Charles (or something like that). Run the app on the simulator and then watch the network connection as the app runs.
Third, if you're still having problems, create a MCVE. Namely, we don't want to see all of your code, but you should instead create the simplest possible example that manifests the problem you describe. Don't ask us to pour through tons of code, but rather make it as absolutely bare-bones as possible.
So I'm not sure why connectionDidFinishLoading is getting called instantly after you set the connection, but I may be able to help you work around the issue.
Try this:
-(UIImage *) getImageFromURL:(NSString *)fileURL {
UIImage * result;
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
result = [UIImage imageWithData:data];
return result;
}
Where fileURL is the a string with the url.
If you want to perform an action after the request is sent try this instead:
-(UIImage *) getImageFromURL:(NSString *)fileURL {
UIImage * result;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
result = [UIImage imageWithData:data];
return result;
dispatch_async(dispatch_get_main_queue(), ^{
//code for operation after download
});
});
}
Let me know how it goes
Ive been trying to figure this out now for over a week and for some reason, no matter what i do, the newsstand icon is not updating via code.
I have read through the Apple developer documentation and tripple checked my image dimensions and they are all correct. I even went and created a new class just for updating the newsstand icon.
Anyway, here is my code:
#import "NewsstandIcon.h"
#import <NewsstandKit/NewsstandKit.h>
#implementation NewsstandIcon
-(void)setIcon:(NSString *)withURL
{
// TEMPORARY OVERRIDE
withURL = #"http://www.imgtag.me/735x1024";
_downloadedData = [[NSMutableData alloc] init];
[_downloadedData setLength: 0];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:withURL]];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
NSLog(#"[ICON DOWNLOAD] Starting download");
}
// DOWNLOAD METHODS
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response {
if ([response statusCode] == 404)
{
NSLog(#"[ICON DOWNLOAD] Failed - Error 404 - File not found");
[connection cancel];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(#"[ICON DOWNLOAD] Received Data");
[_downloadedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"[ICON DOWNLOAD] Completed. Length %d", [_downloadedData length]);
[[UIApplication sharedApplication] setNewsstandIconImage: [[UIImage alloc] initWithData: _downloadedData]];
NSLog(#"[ICON DOWNLOAD] Setting Icon");
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"[ICON DOWNLOAD] Failed - %#", [error description]);
}
#end
And this outputs:
2014-03-08 16:53:24.308 magazine[3590:70b] [ICON DOWNLOAD] Received Data
2014-03-08 16:53:24.308 magazine[3590:70b] [ICON DOWNLOAD] Completed. Length 12833
2014-03-08 16:53:25.892 magazine[3590:70b] [ICON DOWNLOAD] Setting Icon
I also had the downloaded image write to the local file system. I then browsed to the file and the image was fully intact and readable.
As you can see in the code, i have temporarily overwritten the withURL parameter of the setIcon method. I have tried this with many images and none of them work. You can see the file size in my output.
When i call setNewsstandIcon i get no warnings in my output and the icon just stays as the default icon.
Any help would be greatly appreciated!
Thanks.
If your icon doesn't follow the specifications it will result in a nil icon. take a look at this: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/NewsstandIcons.html#//apple_ref/doc/uid/TP40006556-CH28-SW1
note: remember to set a uiimage with scale 2 for retina
I'm trying to get data from a website to display it inside a table view
My code:
-(void)loadTutorials {
NSURL *url = [NSURL URLWithString:[#"http://www.example.com/search?q=" stringByAppendingString:self.searchString]];
NSURLRequest *UrlString = [[NSURLRequest alloc] initWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:UrlString
delegate:self];
[connection start];
NSLog(#"Started");
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
TFHpple *tutorialsParser = [TFHpple hppleWithHTMLData:data];
NSString *tutorialsXpathQueryString = #"//div[#id='header']/div[#class='window']/div[#class='item']/div[#class='title']/a";
NSArray *tutorialsNodes = [tutorialsParser searchWithXPathQuery:tutorialsXpathQueryString];
NSMutableArray *newTutorials = [[NSMutableArray alloc] init];
for (TFHppleElement *element in tutorialsNodes) {
Data *tutorial = [[Data alloc] initWithTitle: [[element firstChild] content]
Url: [#"http://www.example.com" stringByAppendingString: [element objectForKey:#"href"]]
];
[newTutorials addObject:tutorial];
}
_objects = newTutorials;
[self.tableView reloadData];
}
but the data is not showing up, is the data not finished loading?
I got it to working without NSURLConnection but then it will stop the program until the data is recieved
According to NSURLConnectionDataDelegate
connection:didReceiveData:
is called in a incrementally manner.
The newly available data. The delegate should concatenate the contents
of each data object delivered to build up the complete data for a URL
load.
So this means you should append new data within this method.
Then, in
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
you should manipulate your data.
So, for example
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// Create space for containing incoming data
// This method may be called more than once if you're getting a multi-part mime
// message and will be called once there's enough date to create the response object
// Hence do a check if _responseData already there
_responseData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Append the new data
[_responseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Parse the stuff in your instance variable now
}
Obviously you should also implement the delegate responsible for error handling.
A simple note is the following. If data is too big and you need to do some computations stuff (e.g. parsing), you could block the UI. So, you could move the parsing in a different thread (GCD is your friend). Then when finished you could reload the table in the main thread.
Take a look also here for further info: NSURLConnectionDataDelegate order of functions.
I'm running a LOT of asynchronous (delegate, not block) NSURLConnections simultaneously, and they all come back very quickly as I'm hitting a LAN server.
Every so often, one NSURLConnection will go defunct and never return.
connection:willSendRequest: is called but connection:didReceiveResponse: (and failure) is not.
Any ideas? I'm wondering if I should make a simple drop-in replacement using CFNetwork instead.
Edit: There's really not much code to show. What I've done is created a wrapper class to download files. I will note that the problem happens less when I run the connection on a separate queue - but still happens.
The general gist of what I'm doing is creating a download request for each cell as a tableview scrolls (in cellForRowAtIndexPath) and then asynchronously loading in an image file to the table cell if the cell is still visible.
_request = [NSMutableURLRequest requestWithURL:_URL];
_request.cachePolicy = NSURLRequestReloadIgnoringCacheData;
_request.timeoutInterval = _timeoutInterval;
if(_lastModifiedDate) {
[_request setValue:[_lastModifiedDate RFC1123String] forHTTPHeaderField:#"If-Modified-Since"];
}
_connection = [[NSURLConnection alloc] initWithRequest:_request
delegate:self
startImmediately:NO];
[_connection start];
As requested, instance variables:
NSMutableURLRequest *_request;
NSURLConnection *_connection;
And delegate methods:
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {
NSLog(#"%# send", _URL);
return request;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"%# response", _URL);
_response = (id)response;
// create output stream
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
_receivedLength += data.length;
_estimatedProgress = (Float32)_receivedLength / (Float32)_response.expectedContentLength;
[_outputStream write:data.bytes maxLength:data.length];
// notify delegate
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// close output stream
// notify delegate
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"%# failure", _URL);
// notify delegate
}
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if(_credential && challenge.previousFailureCount == 0) {
[[challenge sender] useCredential:_credential forAuthenticationChallenge:challenge];
}
}
After poking around in profiler, I found a lead, and it gave me a hunch.
My credentials were failing (not sure why...) and so previousFailureCount was not 0, and hence I wasn't using my credential object.
Changed the code to this and I have no problems:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if(_credential) {
[[challenge sender] useCredential:_credential forAuthenticationChallenge:challenge];
}
}
A NSURLConnection will send either didReceiveResponse or didFailWithError.
Often, you're dealing with timeouts before didFailWithError occurs.
We have a large project that needs to sync large files from a server into a 'Library' in the background. I read subclassing NSOperation is the most flexible way of multithreading iOS tasks, and attempted that. So the function receives a list of URLs to download & save, initialises an instance of the same NSOperation class and adds each to an NSOperation queue (which should download only 1 file at a time).
-(void) LibSyncOperation {
// Initialize download list. Download the homepage of some popular websites
downloadArray = [[NSArray alloc] initWithObjects:#"www.google.com",
#"www.stackoverflow.com",
#"www.reddit.com",
#"www.facebook.com", nil];
operationQueue = [[[NSOperationQueue alloc]init]autorelease];
[operationQueue setMaxConcurrentOperationCount:1]; // Only download 1 file at a time
[operationQueue waitUntilAllOperationsAreFinished];
for (int i = 0; i < [downloadArray count]; i++) {
LibSyncOperation *libSyncOperation = [[[LibSyncOperation alloc] initWithURL:[downloadArray objectAtIndex:i]]autorelease];
[operationQueue addOperation:libSyncOperation];
}
}
Now, those class instances all get created fine, and are all added to the NSOperationQueue and begin executing. BUT the issue is when it's time to start downloading, the first file never begins downloading (using an NSURLConnection with delegate methods). I've used the runLoop trick I saw in another thread which should allow the operation to keep running until the download is finished. The NSURLConnection is established, but it never starts appending data to the NSMutableData object!
#synthesize downloadURL, downloadData, downloadPath;
#synthesize downloadDone, executing, finished;
/* Function to initialize the NSOperation with the URL to download */
- (id)initWithURL:(NSString *)downloadString {
if (![super init]) return nil;
// Construct the URL to be downloaded
downloadURL = [[[NSURL alloc]initWithString:downloadString]autorelease];
downloadData = [[[NSMutableData alloc] init] autorelease];
NSLog(#"downloadURL: %#",[downloadURL path]);
// Create the download path
downloadPath = [NSString stringWithFormat:#"%#.txt",downloadString];
return self;
}
-(void)dealloc {
[super dealloc];
}
-(void)main {
// Create ARC pool instance for this thread.
// NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init]; //--> COMMENTED OUT, MAY BE PART OF ISSUE
if (![self isCancelled]) {
[self willChangeValueForKey:#"isExecuting"];
executing = YES;
NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:downloadURL];
NSLog(#"%s: downloadRequest: %#",__FUNCTION__,downloadURL);
NSURLConnection *downloadConnection = [[NSURLConnection alloc] initWithRequest:downloadRequest delegate:self startImmediately:NO];
// This block SHOULD keep the NSOperation from releasing before the download has been finished
if (downloadConnection) {
NSLog(#"connection established!");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!downloadDone);
} else {
NSLog(#"couldn't establish connection for: %#", downloadURL);
// Cleanup Operation so next one (if any) can run
[self terminateOperation];
}
}
else { // Operation has been cancelled, clean up
[self terminateOperation];
}
// Release the ARC pool to clean out this thread
//[pool release]; //--> COMMENTED OUT, MAY BE PART OF ISSUE
}
#pragma mark -
#pragma mark NSURLConnection Delegate methods
// NSURLConnectionDelegate method: handle the initial connection
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse*)response {
NSLog(#"%s: Received response!", __FUNCTION__);
}
// NSURLConnectionDelegate method: handle data being received during connection
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[downloadData appendData:data];
NSLog(#"downloaded %d bytes", [data length]);
}
// NSURLConnectionDelegate method: What to do once request is completed
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"%s: Download finished! File: %#", __FUNCTION__, downloadURL);
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDir = [paths objectAtIndex:0];
NSString *targetPath = [docDir stringByAppendingPathComponent:downloadPath];
BOOL isDir;
// If target folder path doesn't exist, create it
if (![fileManager fileExistsAtPath:[targetPath stringByDeletingLastPathComponent] isDirectory:&isDir]) {
NSError *makeDirError = nil;
[fileManager createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&makeDirError];
if (makeDirError != nil) {
NSLog(#"MAKE DIR ERROR: %#", [makeDirError description]);
[self terminateOperation];
}
}
NSError *saveError = nil;
//NSLog(#"downloadData: %#",downloadData);
[downloadData writeToFile:targetPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil) {
NSLog(#"Download save failed! Error: %#", [saveError description]);
[self terminateOperation];
}
else {
NSLog(#"file has been saved!: %#", targetPath);
}
downloadDone = true;
}
// NSURLConnectionDelegate method: Handle the connection failing
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"%s: File download failed! Error: %#", __FUNCTION__, [error description]);
[self terminateOperation];
}
// Function to clean up the variables and mark Operation as finished
-(void) terminateOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
finished = YES;
executing = NO;
downloadDone = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
#pragma mark -
#pragma mark NSOperation state Delegate methods
// NSOperation state methods
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
NOTE: If that was too unreadable, I set up a QUICK GITHUB PROJECT HERE you can look through. Please note I'm not expecting anyone to do my work for me, simply looking for an answer to my problem!
I suspect it has something to do with retaining/releasing class variables, but I can't be sure of that since I thought instantiating a class would give each instance its own set of class variables. I've tried everything and I can't find the answer, any help/suggestions would be much appreciated!
UPDATE: As per my answer below, I solved this problem a while ago and updated the GitHub project with the working code. Hopefully if you've come here looking for the same thing it helps!
In the interests of good community practice and helping anyone else who might end up here with the same problem, I did end up solving this issue and have updated the GitHub sample project here that now works correctly, even for multiple concurrent NSOperations!
It's best to look through the GitHub code since I made a large amount of changes, but the key fix I had to make to get it working was:
[downloadConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
This is called after the NSURLConnection is initialized, and just before it is started. It attaches the execution of the connection to the current main run loop so that the NSOperation won't prematurely terminate before the download is finished. I'd love to give credit to wherever first posted this clever fix, but it's been so long I've forgotten where, apologies. Hope this helps someone!