Know the download progress for update an UIprogressView - iphone

my app downloads a video from internet and saves it in the iPhone.
I'm using this code
NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease];
[request setHTTPMethod:#"GET"];
NSError *error;
NSURLResponse *response;
NSString *documentFolderPath = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *videosFolderPath = [documentFolderPath stringByAppendingPathComponent:#"videos"];
//Check if the videos folder already exists, if not, create it!!!
BOOL isDir;
if (([fileManager fileExistsAtPath:videosFolderPath isDirectory:&isDir] && isDir) == FALSE) {
[[NSFileManager defaultManager] createDirectoryAtPath:videosFolderPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString *filePath = [videosFolderPath stringByAppendingPathComponent:#"name.mp4"];
if ([fileManager fileExistsAtPath:filePath ] == YES) {
NSLog (#"File exists");
}
else {
NSLog (#"File not found");
NSData *urlData;
NSString *downloadPath = #"http://www.mywebsite.com/name.mp4";
[request setURL:[NSURL URLWithString:downloadPath]];
urlData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
BOOL written = [urlData writeToFile:filePath atomically:NO];
if (written)
NSLog(#"Saved to file: %#", filePath);
else {NSLog(#"problem");}
}
All works fine, but I want to add a progress View, to indicate the status of the download.
The problem (I think but I'm not sure) is that my NSURLConnection hasn't itself as delegate, so methods like
- (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)data
or
-(void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite{
are never called.
I tried to use
urlData = [[NSURLConnection alloc] initWithRequest:request delegate:self];
but the app crashes when I try to write the file
[urlData writeToFile:filePath atomically:NO];
What can I do?
Thanks

What you are doing is sending a "synchronous" request, meaning that the thread that is downloading the HTTP response will hang until all data has been fetched. It is never good to do this on a UI thread, even when you do not want display any indicator of the download's progress. I suggest using the NSURLConnection class, setting it's delegate, and responding to delegate methods. A small tutorial for this can be found at http://snippets.aktagon.com/snippets/350-How-to-make-asynchronous-HTTP-requests-with-NSURLConnection.
Once you are a connection's delegate, you can get the content length when the connection receives a response:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
contentSize = [httpResponse expectedContentLength];
}
Then, to calculate the total progress of the download whenever new data arrives, do something like this:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// download data is our global NSMutableData object that contains the
// fetched HTTP data.
[downloadData appendData:data];
float progress = (float)[downloadData length] / (float)contentSize;
[progressView setValue:progress];
}
UPDATE:
Another example on how to do async HTTP with Foundation: http://codewithchris.com/tutorial-how-to-use-ios-nsurlconnection-by-example/

You are using synchronous request here.
The document says -
A synchronous load is built on top of the asynchronous loading code made available by the class. The calling thread is blocked while the asynchronous loading system performs the URL load on a thread spawned specifically for this load request. No special threading or run loop configuration is necessary in the calling thread in order to perform a synchronous load.
Refer the class reference here
After you have read the class reference, you can either send asynchronous request or initialize the request, set delegate, and start.

Related

NSURLConnection data received after connectionDidFinishLoading

I have a question about NSURLConnection:
I want download an image with:
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
the first delegate called (correctly) is
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
after this is called one time:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
and immediately:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
up to here everything is correct, but after connectionDidFinishLoading is fired once again the didReceiveData delegate for the same connection, I identify connection by:
NSString * connectionKey = [[[connection originalRequest] URL] absoluteString];
Is this possible?
update, more infos:
My app fire many simultaneous connections and I store infos for any connection in a dictionary, when a delegate was called I retrive connection info using the key: NSString * connectionKey = [[[connection originalRequest] URL] absoluteString]; for 99% of connections everything is all right but for one (avery time the same!) connection I have this behavior
here the complete implementation:
- (void)downloadFileFromUrl:(NSURL *)url
inPath:(NSString *)completeFilePath
dataReceivedBlock:(void (^)(long long byteDownloaded ,long long totalByte))dataReceivedBlock
endBlock:(void (^)(NSString * downloadPath, NSDictionary * responseHeaders))endBlock
failBlock:(void (^)(NSString * downloadPath, NSDictionary * responseHeaders, NSError * error))failBlock
{
//save the connection infos
NSMutableDictionary * requestData = [[NSMutableDictionary alloc] init];
if(dataReceivedBlock)
[requestData setObject:[dataReceivedBlock copy] forKey:#"dataReceivedBlock"];
if(endBlock)
[requestData setObject:[endBlock copy] forKey:#"endBlock"];
if(failBlock)
[requestData setObject:[failBlock copy] forKey:#"failBlock"];
[requestData setObject:[NSNumber numberWithBool:YES] forKey:#"usingBlock"];
[requestData setObject: completeFilePath forKey:#"downloadDestinationPath"];
//delete the file if already on fs
if([[NSFileManager defaultManager] fileExistsAtPath: completeFilePath])
[[NSFileManager defaultManager] removeItemAtPath:completeFilePath error:nil];
// Create the request.
NSURLRequest * urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLCacheStorageAllowed timeoutInterval:TIME_OUT];
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
if (!theConnection)
{
// Inform the user that the connection failed.
failBlock(completeFilePath,nil,[NSError errorWithDomain:#"Connection fail" code:0 userInfo:nil]);
}
//add connection infos to the requests dictionary
NSString * connectionKey = [[[theConnection originalRequest] URL] absoluteString];
[self.requests setObject:requestData forKey:connectionKey];
}
here a delegate example:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString * connectionKey = [[[connection originalRequest] URL] absoluteString];
NSMutableDictionary * requestData = [self.requests objectForKey:connectionKey];
NSFileHandle *file = [requestData objectForKey:#"fileHandle"];
[file closeFile];
//se la richiesta usa o no block
BOOL usingBlock = [[requestData objectForKey:#"usingBlock"] boolValue];
if(usingBlock)
{
__block void (^endBlock)(NSString * , NSDictionary *) = [requestData objectForKey:#"endBlock"];
if(endBlock)
endBlock([requestData objectForKey:#"downloadDestinationPath"],[requestData objectForKey:#"responseHeaders"]);
}
else
[self.delegate downloadEnded:[requestData objectForKey:#"responseHeaders"]];
//elimino dati richiesta
[self.requests removeObjectForKey:[NSString stringWithFormat:#"%#",connectionKey]];
//Inibisco standby
[UIApplication sharedApplication].idleTimerDisabled = NO;
}
It's impossible. It's must be different connection callback.
ensure each connection use unique delegate, or unique switch branch in the same delegate.
Obviously the solution is really simple -_-
The error is in the connection key identifier:
NSString * connectionKey = [[[connection originalRequest] URL] absoluteString];
my assumption is that key are unique, but if I make x parallel downloadS for the same file at the same url the assumption is wrong ;)

Asynchronous NSURLConnection request responds multiple times

I am facing weired issue. I am sending Asynchronus NSUrlrequest call but in return i am getting multiple time responde with some part of json
can someone please help me with what I did wrong.
code
NSString *_query = #"http://abc.com/index.php";
NSData *myRequestData = [NSData dataWithBytes:[_requestString UTF8String]
length:[_requestString length]];
__block NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:_query]];
[request setHTTPMethod: #"POST" ];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"content-type"];
[request setHTTPBody: myRequestData ];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:timeOutTimer forMode:NSDefaultRunLoopMode];
Response
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// check is response is a valid JSON?
NSError *error;
id jsonObj = [NSJSONSerialization JSONObjectWithData: data options:kNilOptions error:&error];
BOOL isValid = [NSJSONSerialization isValidJSONObject:jsonObj];
NSString *content = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(#"Content: %#",content);
if (isValid)
{
NSDictionary *data = [content JSONValue];
}
[content release];
}
As data is received by the client, this callback gets called:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
didReceiveData is giving you data as it's receiving it and can be called multiple times with chunks of the data.
From the NSURLConnection docs:
The delegate is periodically sent connection:didReceiveData: messages
as the data is received. The delegate implementation is responsible
for storing the newly received data.
From those docs:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Append the new data to receivedData.
// receivedData is an instance variable declared elsewhere.
[receivedData appendData:data];
}
When its all done, connectionDidFinishLoading will get called and your appended data is ready for you to use.
Finally, if the connection succeeds in downloading the request, the
delegate receives the connectionDidFinishLoading: message. The
delegate will receive no further messages for the connection and the
NSURLConnection object can be released.

NSURLConnection not responding

I'm trying to get some data from an URL, but for some reason, nothing happens when I do the following. Neither didReceiveResponse:, didReceiveData:, didFailWithError: or connectionDidFinishLoading: are reached, except when I add a timeout to my request by doing this: [request setTimeoutInterval:10.0]
Here's what I'm doing :
-(void)getConfigFromServer{
[self getContentAtURL:kUrlGetUser];
}
//Va chercher le contenu à l'URL passée en paramètre
- (void)getContentAtURL: (NSURL *)url {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString * userLogin = [userDefaults objectForKey:#"UserLogin"];
NSString * userPassword = [userDefaults objectForKey:#"UserPassword"];
NSURL * urlFinal = [NSURL URLWithString:[NSString stringWithFormat:#"%#", url]];
NSLog(#"Request : %#", [NSString stringWithFormat:#"%#", url]);
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:urlFinal];
[request setHTTPShouldHandleCookies:NO];
[request setTimeoutInterval:10.0];
NSString *sourceString = [NSString stringWithFormat:#"%#:%#", userLogin, userPassword];
NSData * sourceData = [sourceString dataUsingEncoding:NSUTF8StringEncoding];
NSString *authString = [sourceData base64EncodedString];
authString = [NSString stringWithFormat: #"Basic %#", authString];
[request setValue:authString forHTTPHeaderField:#"Authorization"];
[request setValue:#"text/xml" forHTTPHeaderField:#"Accept"];
NSURLConnection * connection=[NSURLConnection connectionWithRequest:request delegate:self];
if(connection){
NSLog(#"Connection started");
receivedData = [NSMutableData data];
}else{
NSLog(#"Error while trying to initiate the connection");
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[receivedData setLength:0];
if ([response respondsToSelector:#selector(statusCode)])
{
int statusCode = [((NSHTTPURLResponse *)response) statusCode];
if (statusCode >= 400)
{
[connection cancel]; // stop connecting; no more delegate messages
NSDictionary *errorInfo
= [NSDictionary dictionaryWithObject:[NSString stringWithFormat:
NSLocalizedString(#"Server returned status code %d",#""),
statusCode]
forKey:NSLocalizedDescriptionKey];
NSError *statusError = [NSError errorWithDomain:#"Error"
code:statusCode
userInfo:errorInfo];
[self connection:connection didFailWithError:statusError];
}
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSLog(#"Connection failed! Error - %# %#",[error localizedDescription],[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self fetchedData:receivedData];
}
EDIT : I'm still having this problem, and now have it on the actual device too. As I said it in the comments, I use ARC on this app for the first time, and I'm using XCode 4.2.
Any idea?
This is old thread but still this answer might help someone out there.
If you are calling this in background thread, check whether you thread is exiting before delegates is called.
Try doing this.
NSURLConnection * connection = [[NSURLConnection alloc]
initWithRequest:request
delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
[connection start];
Error message, as you have told in comments, is saying that the request has timed out.
I am sure that if you remove the line with timeout, you will get either the same response or actual data, after 60 seconds. Make sure you wait enough, because if your connection is very weak for some reason, the request may not time out after 60 seconds because it keeps downloading data.
Is your app in foreground while you launch this? Make sure you don't suspend it.
Furthermore, you say that the server is online, but your code is timing out. Means something is probably wrong with your connection after all.
In another comment you say that sometimes it works. Even more points to the fact that the connection is weak/unreliable or breaking up.
You're passing an NSURL to your getContentAtURL but then treating it as though it was a string in :
NSURL * urlFinal = [NSURL URLWithString:[NSString stringWithFormat:#"%#", url]];
try changing it to:
NSURL * urlFinal = [NSURL URLWithString:[NSString stringWithFormat:#"%#", [url absoluteString]]];
btw, you know you're not using your login and password don't you?
EDIT:
When you say receivedData = [NSMutableData data]; what is data?
I think that might be the problem.
I tend to use the following to setup the data object when data first arrives:
- (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData {
if (receivedData ==nil) { receivedData = [[NSMutableData alloc] initWithCapacity:2048]; }
[receivedData appendData:incrementalData];
}
Some thoughts here:
Please do not store passwords in UserDefaults, use the keychain for that (maybe you are only testing, I just wanted to have this mentioned)
Why do you convert your NSURL url to a new NSURL by calling stringWithFormat: on the url? (Oh, I see #ade already mentioned that)
Is this an ARC-enabled App? If not, your NSMutableData will be autoreleased too soon, use self.receivedData = [NSMutableData data] and you will see a crash.
Try making your NSURLConnection an instance variable and hold on to it. I think it should work the way you do it right now, but I tend to hold on to it and never had the problem you mention here.
Weird that your 10 seconds timeout seems to work, I had trouble getting this to work and found that on iPhone, the timeout can not be lower than 240 seconds. Yes, I was confused about this as well, but: https://stackoverflow.com/a/3611383/148335
I think the problem will be with server side. Try to increase TimeoutInterval to 60 so there will be more time to fetch data from server. Didn't you get "connection timed out" message after 10 seconds?
EDIT
Hi, I think still you are not reached a solution. When I read the code in detail, I found some statements are confusing.
1) [request setValue:#"text/xml" forHTTPHeaderField:#"Accept"]; replace with
[request setValue:#"text/plain" forHTTPHeaderField:#"Accept"];
Also include,
[request setValue:#"text/plain" forHTTPHeaderField:#"Content-Type"];
And finally...
The error "connection timed out" indicates that the iPhone does not receiving any data from server. As soon as iPhone gets a bye of data it will call - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data. Try to put NSLog after each statements to verify variables hold desired value. Try to check the length of data --sourceData-- before sending ...

Does -dataWithContentsOfURL: of NSData work in a background thread?

Does -dataWithContentsOfURL: of NSData work in a background thread?
No, it doesn't.
In order to get data from URL asynchronously you should use the NSURLRequest and NSURLConnection approach.
You will have to implement the NSURLConnectionDelegate methods:
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
-(void)connectionDidFinishLoading:(NSURLConnection *)connection;
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
I'm using dataWithContentsOfURL in a background thread fine.
-(void)loaddata {
NSData* data = [NSData dataWithContentsOfURL:#"some url"];
if (data == nil) {
DLog(#"Could not load data from url: %#", url);
return;
}
}
Call something like this from main thread.
[self performSelectorInBackground:#selector(loaddata) withObject:nil];
If you want to perform updates to ui at end of loaddata, be sure to call a function on main thread.
No. You can use NSURLSession instead, though.
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *imageURL = #"Direct link to your download";
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionDownloadTask *getImageTask = [session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]] completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];
});
}];
[getImageTask resume];
No, it blocks the current thread.
You need to use NSURLConnection in order to have asynchronous requests.
Also you may use -dataWithContentsOfURL + NSOperation + NSOperationQueue
I'm guessing this has changed a bit over the years. But, these days,
NSURLRequest* request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse* response, NSData* data, NSError* error) {
}];
will give you an async network call.
No, this will block the thread and you will load the contents of file into the RAM. You can download content directly into file without temporary NSData to avoid huge RAM usage. Something like this solution https://stackoverflow.com/a/6215458/2937913

Resume download functionality in NSURLConnection

I am downloading some very large data from a server with the NSURLConnection class.
How can I implement a pause facility so that I can resume downloading?
You can't pause, per-se, but you can cancel a connection, and then create a new one to resume where the old left off. However, the server you're connecting to must support the Range header. Set this to "bytes=size_already_downloaded-", and it should pick up right where you cancelled it.
To resume downloading and get the rest of the file you can set the Range value in HTTP request header by doing something like this:
- (void)downloadFromUrl:(NSURL*)url toFilePath:(NSString *)filePath {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10];
if (!request) {
NSLog(#"Error creating request");
// Do something
}
[request setHTTPMethod:#"GET"];
// Add header to existing file
NSFileManager *fm = [NSFileManager defaultManager];
if([fm fileExistsAtPath:filePath]) {
NSError *error = nil;
NSDictionary * fileProp = [fm attributesOfItemAtPath:filePath error:&error];
if (error) {
NSLog(#"Error: %#", [error localizedDescription]);
// Do something
} else {
// Set header to resume
long long fileSize = [[fileProp objectForKey:#"NSFileSize"]longLongValue];
NSString *range = #"bytes=";
range = [[range stringByAppendingString:[[NSNumber numberWithLongLong:fileSize] stringValue]] stringByAppendingString:#"-"];
[request setValue:range forHTTPHeaderField:#"Range"];
}
}
NSURLConnection * connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (!connection) {
NSLog(#"Connection failed.");
// Do something
}
}
Also you can use
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response to check if the existing file is fully downloaded by checking the expected size: [response expectedContentLength];. If sizes match you probably want to cancel the connection.