How do I download a file from a URL in an OS X app? I am a Unix C and Perl programmer new to Objective C. I've been reading the URL Loading System Programming Guide, which suggests NSURLSession for new OS X apps. So I'd prefer a solution with NSURLSession or NSURLSeesionDownloadTask, but am open to NSURLDownload or other solutions.
My first attempt was based an example found here
NSURLSessionDownloadTask *downloadTask =
[[NSURLSession sharedSession]
downloadTaskWithURL:[NSURL URLWithString:pdfFile]
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
// move tmp file to permanent location
NSLog(#"Done...");
NSLog(#"Location: %#", location);
NSLog(#"Response: %#", response);
NSLog(#"Error %#", error);
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL fileCopied = [fileManager moveItemAtPath:[location path] toPath:[appDir stringByAppendingString:#"/demo.pdf"] error:&error];
NSLog(fileCopied ? #"File Copied OK" : #"ERROR Copying file.");
}];
[downloadTask resume];
NSLog(#"Now we are here");
The example code (and my version) don't have a separate delegate. The completionHandler is "in line". (Not sure if that's the correct terminology). I assume this code would be executed AFTER the download task completes. Is that correct?
My second attempt was based on the "Downloading Files" section of the URL Programming Guide:
// PDF file Download, try #2
NSError *error = nil;
NSString *appDir = NSHomeDirectory();
NSString *pdfFile = #"https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/AppDistributionGuide.pdf";
// Configure Cache behavior for default session
NSURLSessionConfiguration
*defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *cachePath = [NSTemporaryDirectory()
stringByAppendingPathComponent:#"/pdfDownload.cache"];
NSLog(#"Cache path: %#\n", cachePath);
NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384
diskCapacity: 268435456 diskPath: cachePath];
defaultConfigObject.URLCache = myCache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
defaultConfigObject.timeoutIntervalForRequest = 100;
defaultConfigObject.timeoutIntervalForResource = 100;
// Create a delegate-Free Session
NSURLSession *delegateFreeSession =
[NSURLSession sessionWithConfiguration: defaultConfigObject
delegate: nil
delegateQueue: [NSOperationQueue mainQueue]];
[[delegateFreeSession downloadTaskWithURL:[NSURL URLWithString:pdfFile] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
// move tmp file to permanent location
NSLog(#"Done...");
NSLog(#"Location: %#", location);
NSLog(#"Response: %#", response);
NSLog(#"Error %#", error);
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL fileCopied = [fileManager moveItemAtPath:[location path] toPath:[appDir stringByAppendingString:#"/demo.pdf"] error:&error];
NSLog(fileCopied ? #"File Copied OK" : #"ERROR Copying file.");
}] resume];
sleep(10);
NSLog(#"After sleep...");
In both cases, code execution never gets to the NSLog statements that are part of the completionHandler. I have breakpoints set in the simulator, and never get there. How do I create a simple "delegate free" file download method?
Since I am writing a "command line" OS X app, I don't have a UIViewController, or other existing structure for delegates.
Any suggestions on how to proceed? I don't need a download progress bar, etc., just a "pass/fail" status.
Also, my example here is for a single PDF file. My app has an array of URLs that I'd like to download. I don't need to download them in parallel. Could I reuse a single NSURLSession in a loop?
Any help would be appreciated!!
Forgot to close this once I got the solution. As Zev pointed out, the PDF file I chose was large, and needed more time to download. The 10 second 'sleep(10)' didn't provide enough time to download the 39 MB file.
Notifications should be used to indicate when the completionHandler finishes. But for now, I am using a simple polling loop. I changed sleep(10); to
int timeoutCounter = 60;
while ((timeoutCounter > 0) && !downloadDone) {
sleep(1);
NSLog(#"Timout: %d", timeoutCounter--);
}
if (!downloadDone) {
NSLog(#"ERROR: Timeout occurred before file completed downloading");
}
This requires the declaration of downloadDone to allow the completionHandler block to set it:
__block BOOL downloadDone = FALSE;
After the NSFileManger moves the file, this semaphore must be set:
downloadDone = TRUE;
Thanks again for the help Zev!
I'm still working on error checking/handling, but here is the full corrected method:
+ (NSError *) downloadFileAtURL:(NSString *)fileURL
toDir:(NSString *)toDir
timeout:(NSNumber *)timeout {
// completion handler semaphore to indicate download is done
__block BOOL downloadDone = FALSE;
// same PDF file name for later
NSString *downloadFileName = [fileURL lastPathComponent];
NSString *finalFilePath = [toDir stringByAppendingPathComponent:downloadFileName];
NSLog(#"PDF file name: '%#'", downloadFileName);
NSLog(#"Final file path: '%#'", finalFilePath);
// Configure Cache behavior for default session
NSURLSessionConfiguration
*defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *cachePath = [NSTemporaryDirectory()
stringByAppendingPathComponent:#"/downloadFileCache"];
NSLog(#"Cache path: %#\n", cachePath);
NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384
diskCapacity: 268435456 diskPath: cachePath];
defaultConfigObject.URLCache = myCache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
defaultConfigObject.timeoutIntervalForRequest = 100;
defaultConfigObject.timeoutIntervalForResource = 100;
// Create a delegate-Free Session
// delegateQueue: [NSOperationQueue mainQueue]];
NSURLSession *delegateFreeSession =
[NSURLSession sessionWithConfiguration: defaultConfigObject
delegate: nil
delegateQueue: nil];
[[delegateFreeSession downloadTaskWithURL:[NSURL URLWithString:fileURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
// move tmp file to permanent location
NSLog(#"In the completion handler...");
NSLog(#"Location: %#", location);
NSLog(#"Response: %#", response);
NSLog(#"Error %#", error);
NSLog(#"Location path: %#", [location path]);
// Verify temp file exists in cache
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ( [fileManager fileExistsAtPath:[location path]]) {
NSLog(#"File exists. Now move it!");
error = nil;
BOOL fileCopied = [fileManager moveItemAtPath:[location path]
toPath:finalFilePath
error:&error];
NSLog(#"Error: %#", error);
NSLog(fileCopied ? #"File Copied OK" : #"ERROR Copying file.");
}
downloadDone = TRUE;
}] resume];
// use timeout argument
int timeoutCounter = [timeout intValue];
while ((timeoutCounter > 0) && !downloadDone) {
sleep(1);
NSLog(#"Timout: %d", timeoutCounter--);
}
if (!downloadDone) {
NSLog(#"ERROR: Timeout occurred before file completed downloading");
}
NSError *error = nil;
return error;
}
Related
I am developing a zip extractor app i followed the algorithm that CRD explained #Here but i stuck at third step i am unable to rename the unzipped file which is at temporary directorary.
here is my code
NSURL *tempDir = [NSURL fileURLWithPath:destinationPath];
NSError *error;
NSURL *tmpDirectory = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tempDir create:YES error:&error];
if (error) {
return ;
}
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
NSLog(#"temp dir %#",tmpDirectory);
NSLog(#"temp path %#",tmpDirectory.path);
[SSZipArchive unzipFileAtPath:zipFilePath toDestination:tmpDirectory.path];
NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory.path error:nil];
NSLog(#"dir file %#",dirFiles);
for (NSString *string in dirFiles) {
NSArray *dirDestinationFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:destinationPath error:nil];
NSLog(#"dir destination file %#",dirDestinationFiles);
[dirDestinationFiles enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error;
if ([string isEqualToString:obj]) {
NSLog(#"Already present");
BOOL isMoved = [fm moveItemAtPath:tmpDirectory.path toPath:[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#-1",string]] error:&error];
if (isMoved) {
NSLog(#"Moved");
}else{
NSLog(#"errorL %#", error);
NSLog(#"Not moved");
}
[fm removeItemAtPath:tmpDirectory.path error:&error];
[self moveFileToTrash:zipFilePath];
[self openExtractedFolderWithZipPath:zipFilePath toDestinationPath:destinationPath];
}
}];
}
Any Suggestions..
Thanks in Advance !
Let's just review your code to hopefully help you on your way.
It may seem minor, but pick good variable names:
NSURL *tempDir = [NSURL fileURLWithPath:destinationPath];
NSURL *tmpDirectory = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tempDir create:YES error:&error];
Two names which are semantically similar for different things, that is just confusing. How about, say, destinationURL instead of tempDir?
Next, when constructing/pulling apart/etc. pathnames or URLs you will be better off being consistent. Both NSURL and NSString provide similar methods for these operations, in one place you use them:
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
but then restort to direct string manipulation using a path separator which may, or may not, be correct:
[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#-1",string]]
The routines provided by NSURL and NSString abstract away from the details of path separators and how to, say, find the extension on the last path component (which you might find useful when renaming to avoid clashes).
Going back to:
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
There is no reason for you to do this. The temporary directory is created for you and you should delete it after using it. So there is no need to create a subdirectory extracts within it, and by reassigning to the same variable you've lost the URL you need to delete the temporary directory.
Now something less obvious, in my comment above I wrote:
To move each item you must handle name clashes, to do this try the move and if you get an error indicating a name clash modify the destination name however you like and re-try the move, repeating until you succeed or you until reach some limit of tries (determined by you).
I didn't explain why you should do it this way and you have tackled the problem a different way: for each item you are going to move you check for names clashes before attempting the move by iterating over the names in the destination directory.
If you read Apple's documentation on the file system you will find they often recommend you try an operation and then examine any error returned instead of trying to predict whether an error will occur and avoid it. The reason for this is the file system is dynamic, other processes can be modifying it, so if you try to avoid an error you may still get one. In pseudocode you are better of doing something like:
moveDone = false
attemptCount = 0
while not moveDone and attemptCount < MAX_ATTEMPTS
move object
if object exists error
modify destination URL
increment attemptCount
else
moveDone = true
end
end
if not moveDone then handle error
Following this outline and using a simple count and the NSString/NSURL path routines will produce you a much simpler and more reliable solution than the one you have now posted as an answer.
HTH
Here is the code working for me.
NSURL *tempDir = [NSURL fileURLWithPath:destinationPath];
NSError *error;
NSURL *tmpDirectory = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tempDir create:YES error:&error];
if (error) {
return ;
}
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
NSLog(#"temp dir %#",tmpDirectory);
NSLog(#"temp path %#",tmpDirectory.path);
[SSZipArchive unzipFileAtPath:zipFilePath toDestination:tmpDirectory.path];
NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory.path error:nil];
NSLog(#"dir file %#",dirFiles);
for (NSString *string in dirFiles) {
NSArray *dirDestinationFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:destinationPath error:nil];
NSLog(#"dir destination file %#",dirDestinationFiles);
NSMutableArray *folderCount = [[NSMutableArray alloc] init];
NSMutableArray *folderNumCount = [[NSMutableArray alloc] init];
[dirDestinationFiles enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj containsString:string]){
[folderNumCount addObject:obj];
}
if ([string isEqualToString:obj]) {
[folderCount addObject:string];
}
}];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error;
if (folderCount.count == 0) {
NSLog(#"First time extract");
BOOL isMoved = [fm moveItemAtPath:tmpDirectory.path toPath:[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#",string]] error:&error];
if (isMoved) {
NSLog(#"Moved");
}else{
NSLog(#"errorL %#", error);
NSLog(#"Not moved");
}
[fm removeItemAtPath:tmpDirectory.path error:&error];
// [self moveFileToTrash:zipFilePath];
// [self openExtractedFolderWithZipPath:zipFilePath toDestinationPath:destinationPath];
}else if (folderCount.count > 0){
NSLog(#"Already present");
BOOL isMoved = [fm moveItemAtPath:tmpDirectory.path toPath:[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#-%lu",string,folderNumCount.count-1]] error:&error];
if (isMoved) {
NSLog(#"Moved");
}else{
NSLog(#"errorL %#", error);
NSLog(#"Not moved");
}
[fm removeItemAtPath:tmpDirectory.path error:&error];
// [self moveFileToTrash:zipFilePath];
// [self openExtractedFolderWithZipPath:zipFilePath toDestinationPath:destinationPath];
}
}
I'm working on recording the voice then change the pitch of the audio and save.
I call this method after recording the voice and click on the button to change the pitch then the new file is also created but the not not able to listen the audio the audio is generated with out voice what may be the error?
-(void)saveEffectedAudioToFolder
{
NSError *error;
if (_audioEngine) {
AVAudioUnitTimePitch *pitchEffect = [AVAudioUnitTimePitch new];
pitchEffect.pitch = pitch;
AVAudioFormat * commonFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 sampleRate:44100 channels:2 interleaved:NO];
//
AVAudioFile *outputFile = [[AVAudioFile alloc] initForWriting:[NSURL URLWithString:_filePathDstaud] settings:commonFormat.settings error:&error];
if (error) {
NSLog(#"Error is 1 %#",[error localizedDescription]);
}
[pitchEffect installTapOnBus: 0 bufferSize: 8192 format: commonFormat block: ^(AVAudioPCMBuffer *buffer, AVAudioTime *when)
//here is that error
{
if (outputFile.length < _audioFile.length)
{
//Let us know when to stop saving the file, otherwise saving infinitely
NSError *error;
NSAssert([outputFile writeFromBuffer:buffer error:&error], #"error writing buffer data to file, %#", [error localizedDescription]);
}else{
_audioEngine = nil;
[pitchEffect removeTapOnBus:0];//if we dont remove it, will keep on tapping infinitely
NSLog(#"Did you like it? Please, vote up for my question");
}
}
];
}
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [documentsPath stringByAppendingPathComponent:name];
[fileManager removeItemAtPath:filePath error:&error];
}
My iCloud Core Data app was running great on iOS7 and ready to launch. When I test on iOS 8 I get the following error and can't seem to fix it when trying to upload data to iCloud.
I suspect my problem is related to how I am getting the document directory and changes in the doc directory with iOS8 but I just can't figure this out..
014-10-12 15:14:17.862 XXXXXXX [4662:236693] __45-[PFUbiquityFilePresenter processPendingURLs]_block_invoke(439): CoreData: Ubiquity: Librarian returned a serious error for starting downloads Error Domain=BRCloudDocsErrorDomain Code=6 "The operation couldn’t be completed. (BRCloudDocsErrorDomain error 6 - Path is outside of any CloudDocs container, will never sync)" UserInfo=0x7f8b1a525f60 {NSDescription=Path is outside of any CloudDocs container, will never sync, NSFilePath=/Users/garyrea/Library/Developer/CoreSimulator/Devices/9AADFE8E-5ECC-4969-9418-57DA45B747C9/data/Containers/Data/Application/AD2E5E62-7295-4371-A08D-1790E8FCCD96/Documents/CoreDataUbiquitySupport/nobody~simA28745A4-A67F-598C-9260-F9AC36609ECF/iCloud/5B8BFA36-1ACA-4966-B7ED-A7344D36ACF1/container/nobody~simA28745A4-A67F-598C-9260-F9AC36609ECF/iCloud/2trlqdMQVpJ~wlEfiLvjWtQfrUJ8YiNCd84KW_xiw4A=/F0CF5F29-D437-4728-B0A2-C5BB90BBC239.1.cdt} with userInfo {
NSDescription = "Path is outside of any CloudDocs container, will never sync";
NSFilePath = "/Users/garyrea/Library/Developer/CoreSimulator/Devices/9AADFE8E-5ECC-4969-9418-57DA45B747C9/data/Containers/Data/Application/AD2E5E62-7295-4371-A08D-1790E8FCCD96/Documents/CoreDataUbiquitySupport/nobody~simA28745A4-A67F-598C-9260-F9AC36609ECF/iCloud/5B8BFA36-1ACA-4966-B7ED-A7344D36ACF1/container/nobody~simA28745A4-A67F-598C-9260-F9AC36609ECF/iCloud/2trlqdMQVpJ~wlEfiLvjWtQfrUJ8YiNCd84KW_xiw4A=/F0CF5F29-D437-4728-B0A2-C5BB90BBC239.1.cdt";
} for these urls: (
"file:///Users/garyrea/Library/Developer/CoreSimulator/Devices/9AADFE8E-5ECC-4969-9418-57DA45B747C9/data/Containers/Data/Application/AD2E5E62-7295-4371-A08D-1790E8FCCD96/Documents/CoreDataUbiquitySupport/nobody~simA28745A4-A67F-598C-9260-F9AC36609ECF/iCloud/5B8BFA36-1ACA-4966-B7ED-A7344D36ACF1/container/nobody~simA28745A4-A67F-598C-9260-F9AC36609ECF/iCloud/2trlqdMQVpJ~wlEfiLvjWtQfrUJ8YiNCd84KW_xiw4A=/F0CF5F29-D437-4728-B0A2-C5BB90BBC239.1.cdt"
)
my app delegate extension code where I create my persistent store is as follows. I have a seed database for first time installation.
- (NSPersistentStoreCoordinator *)createPersistentStoreCoordinator{
NSPersistentStoreCoordinator *persistentStoreCoordinator = nil;
NSManagedObjectModel *managedObjectModel = [self createManagedObjectModel];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:managedObjectModel];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#
"CoreData.sqlite"];
if (![[NSFileManager defaultManager]fileExistsAtPath:[storeURL path]]){
NSURL *preloadURL=[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:#"SeedDatabase" ofType:#
"sqlite"]];
NSError *error=nil;
if (![[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&error]){
NSLog(#
"File couldnt save");
}
}
NSUbiquitousKeyValueStore *kvStore=[NSUbiquitousKeyValueStore defaultStore];
if (![kvStore boolForKey:#"SEEDED_DATA"]){
NSLog (#
"In the new database");
NSURL *seedStoreURL=[NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:#"SeedDatabase" ofType:#
"sqlite"]];
NSError *seedStoreErrpr;
NSDictionary *seedStoreOptions=#{NSReadOnlyPersistentStoreOption: #YES};
NSPersistentStore *seedStore=[persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:seedStoreURL options:seedStoreOptions error:&seedStoreErrpr];
NSDictionary *iCloudOptions =#{NSPersistentStoreUbiquitousContentNameKey: #"iCloud",
NSMigratePersistentStoresAutomaticallyOption:#YES,
NSInferMappingModelAutomaticallyOption:#YES
};
NSOperationQueue *queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSError *error;
[persistentStoreCoordinator migratePersistentStore:seedStore toURL:storeURL options:iCloudOptions withType:NSSQLiteStoreType error:&error];
NSLog(#
"Persistant store migrated");
[kvStore setBool:YES forKey:#
"SEEDED_DATA"];
// [self checkForDuplicates];
}];
}else{
NSError *error;
NSDictionary *storeOptions =#{NSPersistentStoreUbiquitousContentNameKey: #
"iCloud"
};
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:storeOptions
error:&error]) {
NSLog(#
"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
return persistentStoreCoordinator;
}
- (NSURL *)applicationDocumentsDirectory{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
I was able to resolve this error by specifying the iCloud drive directory (Same name as the one on the developer.apple.com interface).
-(NSURL *)cloudDirectory
{
NSFileManager *fileManager=[NSFileManager defaultManager];
NSString *teamID=#"iCloud";
NSString *bundleID=[[NSBundle mainBundle]bundleIdentifier];
NSString *cloudRoot=[NSString stringWithFormat:#"%#.%#",teamID,bundleID];
NSURL *cloudRootURL=[fileManager URLForUbiquityContainerIdentifier:cloudRoot];
NSLog (#"cloudRootURL=%#",cloudRootURL);
return cloudRootURL;
}
and including it in the icloudOptions Dictionary as a NSPersistentStoreUbiquitousContentURLKey
NSDictionary *storeOptions =#{NSPersistentStoreUbiquitousContentNameKey: #"iCloud",
NSPersistentStoreUbiquitousContentURLKey:[self cloudDirectory],
};
I was getting some strange errors so I removed the app from all devices, deleted the iCloud drive file and re ran on an actual device and it worked fine. Not sure if it runs on IOS7 now but since I only specified the NSPersistentStoreUbiquitousContentURLKey I am pretty confident it should be fine.
I had the same issue during loading some test data.
For the load of the data I was deleting all records.
To avoid the exception a simple sleep(1) between cleaning and loading was enough.
I have an app that right now needs to download hundreds of small PDF's based on the users selection. The problem I am running into is that it is taking a significant amount of time because every time it has to open a new connection. I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. Is there a framework that already does this, or is this something I will have to build my self?
This answer is now obsolete. Now that NSURLConnection is deprecated and NSURLSession is now available, that offers better mechanisms for downloading a series of files, avoiding much of the complexity of the solution contemplated here. See my other answer which discusses NSURLSession.
I'll keep this answer below, for historical purposes.
I'm sure there are lots of wonderful solutions for this, but I wrote a little downloader manager to handle this scenario, where you want to download a bunch of files. Just add the individual downloads to the download manager, and as one finishes, it will kick off the next queued one. You can specify how many you want it to do concurrently (which I default to four), so therefore there's no batching needed. If nothing else, this might provoke some ideas of how you might do this in your own implementation.
Note, this offers two advantages:
If your files are large, this never holds the entire file in memory, but rather streams it to persistent storage as it's being downloaded. This significantly reduces the memory footprint of the download process.
As the files are being downloaded, there are delegate protocols to inform you or the progress of the download.
I've attempted to describe the classes involved and proper operation on the main page at the Download Manager github page.
I should say, though, that this was designed to solve a particular problem, where I wanted to track the progress of downloads of large files as they're being downloaded and where I didn't want to ever hold the entire in memory at one time (e.g., if you're downloading a 100mb file, do you really want to hold that in RAM while downloading?).
While my solution solves those problem, if you don't need that, there are far simpler solutions using operation queues. In fact you even hint at this possibility:
I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. ...
I have to say that doing an async download strikes me as the right solution, rather than trying to mitigate the download performance problem by downloading in batches.
You talk about using GCD queues. Personally, I'd just create an operation queue so that I could specify how many concurrent operations I wanted, and download the individual files using NSData method dataWithContentsOfURL followed by writeToFile:atomically:, making each download it's own operation.
So, for example, assuming you had an array of URLs of files to download it might be:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSURL* url in urlArray)
{
[queue addOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
}
Nice and simple. And by setting queue.maxConcurrentOperationCount you enjoy concurrency, while not crushing your app (or the server) with too many concurrent requests.
And if you need to be notified when the operations are done, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self methodToCallOnCompletion];
}];
}];
for (NSURL* url in urlArray)
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
[completionOperation addDependency:operation];
}
[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
[queue addOperation:completionOperation];
This will do the same thing, except it will call methodToCallOnCompletion on the main queue when all the downloads are done.
By the way, iOS 7 (and Mac OS 10.9) offer URLSession and URLSessionDownloadTask, which handles this quite gracefully. If you just want to download a bunch of files, you can do something like:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSFileManager *fileManager = [NSFileManager defaultManager];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSString *finalPath = [documentsPath stringByAppendingPathComponent:filename];
BOOL success;
NSError *fileManagerError;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&fileManagerError];
NSAssert(success, #"removeItemAtPath error: %#", fileManagerError);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&fileManagerError];
NSAssert(success, #"moveItemAtURL error: %#", fileManagerError);
NSLog(#"finished %#", filename);
}];
[downloadTask resume];
}
Perhaps, given that your downloads take a "significant amount of time", you might want them to continue downloading even after the app has gone into the background. If so, you can use backgroundSessionConfiguration rather than defaultSessionConfiguration (though you have to implement the NSURLSessionDownloadDelegate methods, rather than using the completionHandler block). These background sessions are slower, but then again, they happen even if the user has left your app. Thus:
- (void)startBackgroundDownloadsForBaseURL:(NSURL *)baseURL {
NSURLSession *session = [self backgroundSession];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];
}
}
- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:kBackgroundId];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *finalPath = [documentsPath stringByAppendingPathComponent:[[[downloadTask originalRequest] URL] lastPathComponent]];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL success;
NSError *error;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&error];
NSAssert(success, #"removeItemAtPath error: %#", error);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&error];
NSAssert(success, #"moveItemAtURL error: %#", error);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error)
NSLog(#"%s: %#", __FUNCTION__, error);
}
#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
NSLog(#"%s: %#", __FUNCTION__, error);
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
AppDelegate *appDelegate = (id)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
appDelegate.backgroundSessionCompletionHandler();
appDelegate.backgroundSessionCompletionHandler = nil;
});
}
}
By the way, this assumes your app delegate has a backgroundSessionCompletionHandler property:
#property (copy) void (^backgroundSessionCompletionHandler)();
And that the app delegate will set that property if the app was awaken to handle URLSession events:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
self.backgroundSessionCompletionHandler = completionHandler;
}
For an Apple demonstration of the background NSURLSession see the Simple Background Transfer sample.
If all of the PDFs are coming from a server you control then one option would be to have a single request pass a list of files you want (as query parameters on the URL). Then your server could zip up the requested files into a single file.
This would cut down on the number of individual network requests you need to make. Of course you need to update your server to handle such a request and your app needs to unzip the returned file. But this is much more efficient than making lots of individual network requests.
Use an NSOperationQueue and make each download a separate NSOperation. Set the maximum concurrent operations property on your queue to however many downloads you want to be able to run simultaneously. I'd keep it in the 4-6 range personally.
Here's a good blog post that explains how to make concurrent operations.
http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/
What came as a big surprise is how slow dataWithContentsOfURL is when downloading multiple files!
To see it by yourself run the following example:
(you don't need the downloadQueue for downloadTaskWithURL, its there just for easier comparison)
- (IBAction)downloadUrls:(id)sender {
[[NSOperationQueue new] addOperationWithBlock:^{
[self download:true];
[self download:false];
}];
}
-(void) download:(BOOL) slow
{
double startTime = CACurrentMediaTime();
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
static NSURLSession* urlSession;
if(urlSession == nil)
urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
dispatch_group_t syncGroup = dispatch_group_create();
NSOperationQueue* downloadQueue = [NSOperationQueue new];
downloadQueue.maxConcurrentOperationCount = 10;
NSString* baseUrl = #"https://via.placeholder.com/468x60?text=";
for(int i = 0;i < 100;i++) {
NSString* urlString = [baseUrl stringByAppendingFormat:#"image%d", i];
dispatch_group_enter(syncGroup);
NSURL *url = [NSURL URLWithString:urlString];
[downloadQueue addOperationWithBlock:^{
if(slow) {
NSData *urlData = [NSData dataWithContentsOfURL:url];
dispatch_group_leave(syncGroup);
//NSLog(#"downloaded: %#", urlString);
}
else {
NSURLSessionDownloadTask* task = [urlSession downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//NSLog(#"downloaded: %#", urlString);
dispatch_group_leave(syncGroup);
}];[task resume];
}
}];
}
dispatch_group_wait(syncGroup, DISPATCH_TIME_FOREVER);
double endTime = CACurrentMediaTime();
NSLog(#"Download time:%.2f", (endTime - startTime));
}
There is nothing to "build". Just loop through the next 10 files each time in 10 threads and get the next file when a thread finishes.
I am using AVFoundation to capture video. All seems to be well in my app until I get to here:
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections
error:(NSError *)error
{
NSLog(#"didFinishRecordingToOutputFileAtURL - enter");
BOOL RecordedSuccessfully = YES;
if ([error code] != noErr)
{
// A problem occurred: Find out if the recording was successful.
id value = [[error userInfo] objectForKey:AVErrorRecordingSuccessfullyFinishedKey];
if (value)
{
RecordedSuccessfully = [value boolValue];
}
}
if (RecordedSuccessfully)
{
//----- RECORDED SUCESSFULLY -----
NSLog(#"didFinishRecordingToOutputFileAtURL - success");
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputFileURL])
{
[library writeVideoAtPathToSavedPhotosAlbum:outputFileURL
completionBlock:^(NSURL *assetURL, NSError *error)
{
if (error)
{
}
}];
}
[library release];
}
}
When running it on a device I receive the "didFinishRecordingToOutputFileAtURL - success" message, and then it crashes and I get this error:
Video /private/var/mobile/Applications/EDC62CED-3710-45B2-A658-A2FE9238F517/tmp/output.mov cannot be saved to the saved photos album: Error Domain=NSOSStatusErrorDomain Code=-12950 "Movie could not be played." UserInfo=0xe6749b0 {NSLocalizedDescription=Movie could not be played.}
I haven't been able to find a whole lot of information about this anywhere and am not really sure what's going on.
Here's the code with the temporary output URL:
NSString *outputPath = [[NSString alloc] initWithFormat:#"%#%#", NSTemporaryDirectory(), #"output.mov"];
NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:outputPath];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:outputPath])
{
NSError *error;
if ([fileManager removeItemAtPath:outputPath error:&error] == NO)
{
//Error - handle if requried
}
}
[outputPath release];
//Start recording
[movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];
[outputURL release];
I've already tried releasing the outputURL elsewhere after it saves, and that didn't help, so I don't think that's it.
I figured this out. I was an idiot and kept overlooking the fact that I didn't actually connect the video input to the capture session. Whoops.