Download many files (photos, videos) in background with priority order - ios

At first start of app I want to download all files from server and I want to continue in downloading even when user leaves app (it's not in foreground). The files I need to download are thumbnails, photos in original size, other files and video. I want to download them in order as I wrote before.
I am using Alamofire and I set session manager:
let backgroundManager: Alamofire.SessionManager = {
let bundleIdentifier = "com....."
return Alamofire.SessionManager(
configuration: URLSessionConfiguration.background(withIdentifier: bundleIdentifier + ".background")
)
}()
Then I am using it like this:
self.backgroundManager.download(fileUrl, to: destination)
.downloadProgress { progress in
//print("Download Progress: \(progress.fractionCompleted)")
}
.response(completionHandler: result)
It's in downloadPhoto method and I am calling it:
for item in items {
self.downloadPhoto(item: item, isThumbnail: true, shouldReloadData: false, indexPath: nil)
self.downloadPhoto(item: item, isThumbnail: false, shouldReloadData: false, indexPath: nil)
}
Then I could add call for file download and video download and other. But all this requests have same priority and I would like to download thumbnails first (because that's what user see at first) then full size images and after all images are downloaded then files and videos. But all must be in queue because if user starts app and then set it to background and left it for several hours all must be downloaded. Is this possible? And how can I do this?
I was looking at alamofire that it has component library AlamofireImage which has priority based downloading but images are just part of files which I want to prioritize. Thanks for help

Have a look at TWRDDownloadManager.
It uses NSURLSessionDownloadTask and also supports background modes.
What you need to do is:
1. Set HTTPMaximumConnectionsPerHost to 1 to be sure that download happen serially:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.timeoutIntervalForRequest = 30.0;
configuration.HTTPMaximumConnectionsPerHost = 1; // Note this
2. Following is the method which iterate for loop and download media one by one:
-(void)downloadDocuments
{
for(int i=0; i<[arrDownloadList count]; i++)
{
Downloads *download = [arrDownloadList objectAtIndex:i];
NSString *fileURL = download.documentURL;
[[TWRDownloadManager sharedManager] downloadFileForURL:fileURL
withName:[fileURL lastPathComponent]
inDirectoryNamed:kPATH_DOC_DIR_CACHE_DOCUMENTS
completionBlock:^(BOOL completed)
{
if (completed)
{
/* To some task like database download flag updation or whatever you wanr */
downloadIndex++;
if(downloadIndex < [arrDownloadList count]) {
[self updateDownloadingStatus];
}
else {
[self allDownloadCompletedWithStatus:TRUE];
}
}
else
{
/* Cancel the download */
[[TWRDownloadManager sharedManager] cancelDownloadForUrl:fileURL];
downloadIndex++;
if(downloadIndex < [arrDownloadList count]) {
[self updateDownloadingStatus];
}
else {
[self allDownloadCompletedWithStatus:TRUE];
}
}
} enableBackgroundMode:YES];
}
}
Don't forget to enable background mode: enableBackgroundMode:YES
3. Enable Background Modes in Xcode:
4. Add the following method to your AppDelegate:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
[TWRDownloadManager sharedManager].backgroundTransferCompletionHandler = completionHandler;
}
If you do this, the downloading will occur serially and it will continue even if App is in background or user locks the device.
Please add comment in case of any question or help regarding this.

I believe that iOS standard URLSessionDownloadTask will help you. It has property priority, which looks like you need.
Here is official documentation
Here is some tutorial

Related

Implementing auto loading data from express.js server to Objective-C mobile app

I have an app written in Objective-C that needs to autoload some data from a node.js server.
Application 1 sends a message to my server that Application 2 then needs to receive. Application 2 needs to load this message automatically (no refresh buttons). These messages are common data and not Remote Notifications with APN.
I currently use the following:
- (void) checkForNewMessages {
// call the method on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self->dataParser getMessages:^(NSArray *arr, NSError *error) {
// If there's an error
if (error) {
return;
}
// otherwise
[self->messageArray removeAllObjects];
self-> messageArray = [NSMutableArray arrayWithArray:arr];
if (self-> messageArray) { // is not nil
// update UI on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self->newMessageTimer invalidate];
[self tableRefresh];
});
}
}];
});
}
which uses a GET request to pull the messages (controlled by a repeating 10-second timer that invalidates when messages are found).
I figured this would work as it runs in the background - but this has become quite buggy, and relies on a timer - it also often freezes the UI while the request is taking place, even though it should be running in the background.
Essentially, is there a better solution to this type of functionality - or does this seem perfectly valid for a production app.
With DISPATCH_QUEUE_PRIORITY_DEFAULT you allow the system to select a queue which to use and sometimes it can decide to choose main queue, as it is free enough. So in your case it is better approach to specify explicitly that you need background queue, as below
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[self->dataParser getMessages:^(NSArray *arr, NSError *error) {
...

Tapping to cancel a Download

I want to do the following:
1)DownLoad a series of files whose URLs are stored in an NSMutableArray.
2)During the download process a MBProgressHUD shows the download status.
3)At any point of download I want to cancel the download, when the user touches the screen.
-(void)singleTap:(UITapGestureRecognizer*)sender
{
NSLog(#"%#",#"tapped");
self.downLoadHud.detailsLabelText=#"";
self.downLoadHud.labelText=[SAGlobal stringForValue:#"CANCELLINGDOWNLOAD"];
SharedAppDelegatee.downLoadCancelFlag=YES;
}
-(void) startFileDownLoadingWithHUD
{
self.downLoadHud=[MBProgressHUD showHUDAddedTo:[SharedAppDelegatee window] animated:YES];
self.downLoadHud.mode = MBProgressHUDModeIndeterminate;
UITapGestureRecognizer *HUDSingleTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(singleTap:)];
[self.downLoadHud addGestureRecognizer:HUDSingleTap];
self.downLoadHud.labelText = #"Initialising..";
self.downLoadHud.detailsLabelText =#"";
[self.downLoadHud setColor:[UIColor blackColor]];
dispatch_queue_t dispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(dispatchQueue, ^(void)
{
[self startAllFilesInArrayDownload];
//downloadcancelled or downloadfinished
while (!(SharedAppDelegatee.downLoadCancelFlag)||(SharedAppDelegatee.SAdownloadMode==0)) {
self.downLoadHud.labelText =[NSString stringWithFormat:#"Downloading..[%d/%d]",self.self.downloadErrorCount+self.downloadSuccessCount,[filesToDownLoad count]];
self.downLoadHud.detailsLabelText =[SAGlobal stringForValue:#"TAPTOCANCEL"];
//NSLog(#"DOWNLOADING------+");
}
////////////////////////////////
dispatch_sync(dispatch_get_main_queue(), ^{
[self.downLoadHud hide:YES];
//downLoadHud
});
});
}
The download is done with
for (downDict in filesToDownLoad)
{
//[adm downloadURL:[downDict objectForKey:#"url"] destPath:[downDict objectForKey:#"toFile"]];
NSURL *aUrl = [NSURL URLWithString:[downDict objectForKey:#"url"]];
[self.downloadManager addDownloadWithFilename:[downDict objectForKey:#"toFile"] URL:aUrl];
//[urlStringsArray addObject:[downDict objectForKey:#"url"]];
}
The "downloadManager" is an object of class "DownloadManager" which is obtained
https://github.com/robertmryan/download-manager
I could succesively download all files. I am NOT able to cancel the download in the middle of download. When the user taps the button, it waits a long time, and after some time, the "singleTap" method is called.
The number of files downloaded and failed are CORRECTLY shown. What is wrong with my code?. Can any one suggest me a better example or way to handle "Showing a busy hud + downloading+ tap to cancel feature similar as shown below.
While I am certain that this is a great utility and a lot of work went into it I was immediately concerned when i saw that the repo was two years old.
These types of things are a great help and i can't begin to express my appreciation for the authors and their generosity in sharing so much hard work. Unfortunately, if they are not maintained then they can become difficult for the user to update.
Apple has more recently introduced new functionality with NSURLSession.
This is pretty easy to use and is much more powerful than NSURLConnection.
It specifically includes the ability to pause, resume and cancel network downloads.

sync api download image with progress bar in ios

Hi i am using sync api for download images from DBx and sync my app data to DBX. Now i want to download images from DBX with progress bar. I tried this but i could not any progress value and get downloaded with this warning. This is my code
DBFile *orignalImg = [[DBFilesystem sharedFilesystem]openFile:imgInfo.imgPath error:nil];
__weak DBFile *oFile = orignalImg;
if (oFile)
{
[orignalImageArray addObject:oFile]; // make reference of file
}
if (orignalImg.status.cached)
{
// already displayed
}
else
{
[orignalImg addObserver:self block:^(void)
{
DBFileStatus *fileStatus = oFile.status;
DBFileStatus *newerStatus = oFile.newerStatus;
UIImage *aImage = [UIImage imageWithData: [oFile readData:nil]];
if (fileStatus.cached) // if image downloaded
{
//display image
}
else if (fileStatus.state == DBFileStateDownloading) // show progress bar
{
// show progress
[self showProgress:strPath andProgressValue:fileStatus.progress];
}
else if (newerStatus && newerStatus.state == DBFileStateDownloading)// show progress bar
{
[self showProgress:strPath andProgressValue:fileStatus.progress]; }
}];
}
Warning is :- dropbox_file_wait_for_ready should not be called on the main thread
#rmaddy is right... calling readData before the file has finished downloading will cause that call to block, and so you won't see any progress. (That's also presumably causing the warning.)
If you don't do that, you should be able to see progress as the file downloads, but it looks like you haven't implemented that part yet.

Is ALAssetsLibrary thread safe (Deadlock occurs when using multiple threads)

I'm currenly working on a tiny project which amis to loading ALL gallery photos into my app to show some fancy effect. Unfortunately, these default thumbnail provided by system cannot meet my requirement. So I try to create my own thumbnails using "fullScreenImage". To speed up the process, I load fullScreenImage using background operations. The main methods are:
- (void)getFullScreenImage:(NSURL *)url success:(void(^)(UIImage *))callback
{
NSLog(#"Requesting %#", url);
[assetsLibraryInstance assetForURL:url resultBlock:^(ALAsset *asset) {
callback(asset.defaultRepresentation.fullScreenImage);
}
failureBlock:nil];
}
- (void)processURLs:(NSArray *)urls
{
for (NSURL *url in urls) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ^{
[self getFullScreenImage:url success:^(UIImage *img) {
NSLog(#"Got image %#", img);
}] ;
});
}
}
Only the "Requesting..." log is printed in console, the "getFullScreenImage" method is locked, no any output.
I tried below methods to work around this issue:
Not sharing assetsLibraryInstance (Didn't work)
Don't dispatch_async when enumeate urls in "processURLs". (Did work, but I don't want to use a signle thread to process all URLs)
Not using global queue, using main queue (Did work, but all these "fullScreenImage" works were doing on UI thread, making UI non-responsive)
Using a private queue created with "dispatch_queue_create". (Didn't work)
So, is ALAssetsLibrary thread safe? I guess it's not... Or, is there any better way I can use to:
Load fullScreenImage in background
Multiple threading
Thanks!

AVURLAsset gets initialized properly, but sometimes its associated AVPlayerLayer just renders black

I have an application in which the user can select from local video files. When one of those thumbnails gets pushed, the user is presented a new view which have a custom video player I've made that presents the video.
This works flawlessly, but only sometimes. The funny thing is that if the user selects a new video (thus getting presented a new view, initializing a new custom video player object) exactly 5 times, the underlying AVPlayerLayer that is used to present the visuals from the player renders black, even though it seems like the underlying asset still loads correctly (the player interface still holds the correct duration for the video and so forth).
When a new custom media player object gets initialized (which happens when the view controller for the media players containing view gets loaded), this is the part of the initializer method which sets up the AVPlayer and its associated item:
// Start to load the specified asset
mediaAsset = [[AVURLAsset alloc] initWithURL:contentURL options:nil];
if (mediaAsset == nil)
NSLog(#"The media asset is zero!!!");
// Now we need to asynchronously load in the tracks of the specified asset (like audio and video tracks). We load them asynchronously to avoid having the entire app UI freeze while loading occours
NSString* keyValueToLoad = #"tracks";
// When loading the tracks asynchronously we also specify a completionHandler, which is the block of code that should be executed once the loading is either or for some reason failed
[mediaAsset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:keyValueToLoad
] completionHandler:^
{
// When this block gets executed we check for potential errors or see if the asset loaded successfully
NSError* error = nil;
AVKeyValueStatus trackStatus = [mediaAsset statusOfValueForKey:keyValueToLoad error:&error];
if (error != nil)
{
NSLog(#"Error: %#", error.description);
}
//switch (trackStatus) {
//case AVKeyValueStatusLoaded:
if (trackStatus == AVKeyValueStatusLoaded)
{
NSLog(#"Did load properly!");
mediaItem = [AVPlayerItem playerItemWithAsset:mediaAsset];
if (mediaItem.error == nil)
NSLog(#"Everything went fine!");
if (mediaItem == nil)
NSLog(#"THE MEDIA ITEM WAS NIL");
[mediaItem addObserver:self forKeyPath:#"status" options:0 context:&itemStatusContext];
mediaContentPlayer = [[AVPlayer alloc] initWithPlayerItem:mediaItem];
[mediaContentView setPlayer:mediaContentPlayer];
//mediaContentView = [AVPlayerLayer playerLayerWithPlayer:mediaContentPlayer];
[activeModeViewBlocked configurePlaybackSliderWithDuration:mediaItem.duration];
originalDuration = mediaItem.duration.value / mediaItem.duration.timescale;
// We will subscribe to a timeObserver on the player to check for the current playback time of the movie within a specified interval. Doing so will allow us to frequently update the user interface with correct information
playbackTimeObserver = [mediaContentPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 50) queue:dispatch_get_main_queue() usingBlock:^(CMTime time)
{
NSLog(#"TIME UPDATED!");
[activeModeViewBlocked updatePlaybackSlider:time];
}];
[self syncUI];
}
if (trackStatus == AVKeyValueStatusFailed)
{
NSLog(#"Something failed!");
}
if (trackStatus == AVKeyValueStatusCancelled)
{
NSLog(#"Something was cancelled!");
}
}];
Now if I initialize this custom media player object 5 times exactly, it always starts to render black screens.
Does anyone have any idea of why this could be happening?
This bit me too. There is a limit on the number of concurrent video players that AVFoundation will allow. That number is four(for iOS 4.x, more recently the number seems to have increased. For example, on iOS 7 I've had up to eight on one screen with no issue). That is why it is going black on the fifth one. You can't even assume you'll get four, as other apps may need a 'render pipeline'.
This API causes a render pipeline:
+[AVPlayer playerWithPlayerItem:]

Resources