I am downloading a bunch of largish zip files with the following method. It can take a little while and so I'd like to display a progress bar.
I've researched how to do with with the delegate methods for NSURLConnection and it seems straightforward, however I want to achieve the same thing with "sendAsynchronousRequest". How can I get the number of bytes downloaded as it downloads as well as the total number of bytes expected so that I can display a progress bar? I understand that I cannot use the delegate methods if I kick off a download in the manner I am doing it.
// Begin the download process
- (void)beginDownload:(NSMutableArray *)requests {
// Now fire off a bunch of requests asynchrounously to download
self.outstandingRequests = [requests count];
for (NSURLRequest *request in requests) { // Get the request
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// Error check
if ( error != nil ) {
// The alertview for login failed
self.appDelegate.warningView.title = #"Refresh Error!";
self.appDelegate.warningView.message = [error localizedDescription];
// Show the view
[self.appDelegate.warningView show];
// Debug
if ( DEBUG ) {
NSLog(#"A request failed - %d left!",self.outstandingRequests);
}
}
else {
// Debug
if ( DEBUG ) {
NSLog(#"A request is done - %d left!",self.outstandingRequests);
}
}
// Decrement outstanding requests
self.outstandingRequests--;
// No requests are left
if (self.outstandingRequests == 0) {
// Debug
if ( DEBUG ) {
NSLog(#"All requests are done!");
}
// Get rid of loading view
[self performSelector:#selector(dismissLoadingView) withObject:nil afterDelay:0.15];
}
}];
}
}
https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLConnectionDownloadDelegate_Protocol/NSURLConnectionDownloadDelegate/NSURLConnectionDownloadDelegate.html#//apple_ref/doc/uid/TP40010954-CH2-SW1
How to make an progress bar for an NSURLConnection when downloading a file?
http://iphonedevsdk.com/forum/iphone-sdk-development/24233-nsurlconnection-with-uiprogressbar.html
http://iphoneeasydevelopment.blogspot.com/2011/10/use-progess-bar-when-downloading-file.html
sendAsynchronousRequest won't work for your purposes as it doesn't call your callback until the request has completed. You'll need to use initRequest:withDelegate: and handle your own data accumulation.
When the header is received (possibly multiple times for redirects) your didReceiveResponse method will be called, you can pick up the expected size there:
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
_expectedBytes = (NSUInteger)response.expectedContentLength;
_data = [NSMutableData dataWithCapacity:_expectedBytes];
// make a progress update here
}
You'll receive a call to the delegate method didReceiveData each time a chunk of data is received, so you know how much data you've received up to this point.
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_data appendData:data];
_receivedBytes = _data.length;
// make a progress update here
}
Guys I'm working on the Newsstand stuff now. I'm trying to handle network errors.
What you see on the image below is my simple log ("Percentage: %i" is inside connection:didWriteData:totalBytesWritten:expectedTotalBytes:).
My problem is depicted in the last 3 lines of code.
What I've done in this lines:
After that line I've switched on the airplane mode (simulated network error)
I've received connection:didWriteData:totalBytesWritten:expectedTotalBytes: with totalBytesWritten equal to expectedTotalBytes
I've received connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL.
After that:
Hooray, I've just finished downloading my .zip, I can unpack it, announce the status to my view and so on... :(
My question is what's going on?
I have implemented connection:didFailWithError: but it's not invoked.
I was trying to grab the totalBytesWritten in last invoked didWriteData: and compare it to real file size in DidFinishDownloading:
I have stripped all my project away just to make sure that its not related to my whole design.
I'm thinking about combination of NSTimer and NKIssueContentStatusAvailable to check the real download status.
It's all hacky. Isn't it?
Update:
Reproduced on iOS 6 and 7 with XCode 5
All NewsstandKit methods invoked on the main thread
Same thing when simulating offline mode with Charles proxy (app in foreground)
It's not an issue anymore when switching to Airplane, but still can reproduce the issue when throttling on Charles proxy.
I ended up with this solution (checking if connection:didWriteData:... is telling the truth in connectionDidFinishDownloading:destinationURL:):
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes
{
...
self.declaredSizeOfDownloadedFile = expectedTotalBytes;
}
And:
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL
{
NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:destinationURL.absoluteString error:nil];
NSNumber* destinationFileSize = [fileAttributes objectForKey:NSFileSize];
if (destinationFileSize.intValue != self.declaredSizeOfDownloadedFile)
{
NSError* error = ...;
[self connection:connection didFailWithError:error];
self.declaredSizeOfDownloadedFile = 0;
return;
}
...
}
I wish to fetch data for an array of URLs that return JSON data. I am trying the following code:
for (int i =0; i<numberOfDays; i++)
{
NSData *data = [NSData dataWithContentsOfURL:[wordURLs objectAtIndex:i]];
NSLog(#"%#",[wordURLs objectAtIndex: i]);
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];
}
'wordURLs' are the array of URLs and in my 'fetchedData:' method, I save the returned JSON data to a plist file.
The issue is that for all number of times that the loop runs, the data is returned for only one/two particular URLs (i.e. say for the urls at indices at 1 and 3, or 1 and 2 etc). I log and see that the URLs are different for each time the 'data' variable is initialized.
What is a better way of doing this?
I have used NSJSONSerialization for parsing JSON.
There are much better ways of doing this. The problem with what you are trying to do is that it is synchronous, which means your app will have to wait for this action to be completed before it can do anything else. I definitely would recommend looking into making this into an asynchronous call by simply using NSURLConnection and NSURLRequests, and setting up delegates for them.
They are relatively simple to set up and manage and will make your app run a million times smoother.
I will post some sample code to do this a little later once I get home.
UPDATE
First, your class that is calling these connections will need to be a delegate for the connections in the interface file, so something like this.
ViewController.h
#interface ViewController: UIViewController <NSURLConnectionDelegate, NSURLConnectionDataDelegate> {
NSMutableData *pageData;
NSURLConnection *pageConnection;
}
Then you will need to create/initialize the necessary variables in you implementation
ViewController.m
-(void) viewDidLoad {
pageData = [[NSMutableData alloc] init];
NSURLRequest *pageRequest= [[NSURLRequest alloc] initWithURL:pageURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:4];
pageConnection = [[NSURLConnection alloc] initWithRequest:pageRequestdelegate:self];
}
Then you also need the delegate functions that will get called as the data is retrieved.
-(void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
if (connection == pageConnection) {
[pageData appendData:data];
}
}
-(void) connectionDidFinishLoading:(NSURLConnection *)connection {
if (connection == pageConnection) {
// Do whatever you need to do with the data
}
}
-(void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (connection == pageConnection) {
// Do something since the connection failed
}
}
Of course this example only includes one URL being loaded, but you could make it as many as you want. You will of course have to keep track of all of the necessary NSURLConnections so you know where to put the data you received, as well as what actions to take in case of a failure or the connection being completed successfully, but that is not a hard extension from what I have given.
If you see any glaring errors or something does not work, please let me know.
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
I am trying to download data from internet to NSData in iOS.
When I download data from internet , I can't see how many percentage downloaded from server.
I'm not using UIWebView.
download sound (.mp3) from Internet with NSData.
Is there anyways can I know how much data downloaded from internet?
Thanks in advance.
Steps:
1) Create a NSURLConnection with a request to the .mp3's URL.
2) Set self as the delegate of this connection.
3) Implement the NSURLConnectionDataDelegate protocol. ( Add next to your class's interface declaration.
4) Implement these methods:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
statusCode = [httpResponse statusCode];
if ((statusCode/100) == 2)
{
contentLength = [httpResponse expectedContentLength];
if (contentLength == NSURLResponseUnknownLength)
NSLog(#"unknown content length %ld", contentLength);
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
bytesSum += [data length];
percent = (float)bytesSum / (float)contentLength;
// append the new data to the receivedData
[receivedData appendData:data]; //received data is a NSMutableData ivar.
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//the end. Write your data ( stored in receivedData ) to a local .mp3 file.
}
Hope this helps.
Cheers!
I would suggest to have a look at this: ASIHTTPRequest
You could easily track upload and download process with it and other nice stuff.
Sebastian