iOS Share Extension - Sending Large Video - ios

So I am trying send a large video file (over 100 mb), and whenever I access the video file with dataWithContentsOfURL, the extension terminates. This works fine with smaller files.
How am I supposed to work around it?
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]){
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeMovie options:nil completionHandler:urlHandler];
}
NSItemProviderCompletionHandler urlHandler = ^(NSURL *item, NSError *error) {
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] | [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie])
{
NSData *fileData = [NSData dataWithContentsOfURL:item]
// ----> fileData WORKS for small files.
// ----> for large files, extension QUITS - without any trace - and control does not proceed after this. This may be due to memory pressure?
[_shareExtensionActionsManager sendTextMessage:contentText attachmentData:fileData attachmentName:#"video-1" toChatEntity:_selectedItem completion:^(BOOL success)
{
[self.extensionContext completeRequestReturningItems:nil completionHandler:^(BOOL expired) {
exit(0);
}];
}];
}
};

From the app extension docs:
Users tend to return to the host app immediately after they finish their task in your app extension. If the task involves a potentially lengthy upload or download, you need to ensure that it can finish after your extension gets terminated.
and
After your app extension calls completeRequestReturningItems:completionHandler: to tell the host app that its request is complete, the system can terminate your extension at any time.
You will need to use NSURLSession to create a URL session that initiates a background task.
If your extension isn’t running when the background task completes, the system will launch your containing app in the background and call application:handleEventsForBackgroundURLSession:completionHandler: in your AppDelegate.
You'll also need to setup a shared container that both your extension and the containing app can access. For this, you'll want to use NSURLSessionConfiguration's sharedContainerIdentifier property to specify an identifier for the container so you can access it later.
Here's a sample from the docs that shows how you can achieve this:
NSURLSession *mySession = [self configureMySession];
NSURL *url = [NSURL URLWithString:#"http://www.example.com/LargeFile.zip"];
NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];
[myTask resume];
- (NSURLSession *) configureMySession {
if (!mySession) {
NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#“com.mycompany.myapp.backgroundsession”];
// To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
config.sharedContainerIdentifier = #“com.mycompany.myappgroupidentifier”;
mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return mySession;
}
Here's a related resource that might help further.

App extensions may not have the memory capacity for this task.
Memory limits for running app extensions are significantly lower than the memory limits imposed on a foreground app. On both platforms, the system may aggressively terminate extensions because users want to return to their main goal in the host app. Some extensions may have lower memory limits than others: For example, widgets must be especially efficient because users are likely to have several widgets open at the same time.
https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionCreation.html#//apple_ref/doc/uid/TP40014214-CH5-SW1

Related

How to get use BGTask in objective-c with iOS 13(Background Fetch)

So i want to build a iOS, i am pretty new to the world of objective-c and one feature i want to implement is the ability to send a API request and do a bit of background processing while the app is not "in focus/in background". I have researched for a couple days about this BGTask API for iOS 13 and have created a projected to see if i can get "background fetch" working. I have not be able to. Im pretty sure i have everything setup correctly but i can not get background fetch functionality to trigger on my iPhone, not even once over the past couple days.
I am using a actual iOS device to test this with iOS 13.4.1
"Permitted background task scheduler identifiers" is setup properly in Info.plist
App is signed
Background processing and Background fetch is checked in Background Modes
I waited the 15 minute interval as per Apples documentation
Here is my code. All this is just a blank iOS project using objective-c. I only edited AppDelegate.m and Info.plist
AppDelegate.m
#import "AppDelegate.h"
#import <BackgroundTasks/BackgroundTasks.h>
static NSString* TaskID = #"com.myapp.task";
#interface AppDelegate ()
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:TaskID
usingQueue:nil
launchHandler:^(BGProcessingTask *task) {
[self handleAppRefreshTask:task];
}];
return YES;
}
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return [[UISceneConfiguration alloc] initWithName:#"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
-(void)handleAppRefreshTask:(BGProcessingTask *)task {
//do things with task
NSLog(#"Process started!");
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
NSString *targetUrl = #"https://webhook.site/1b274a6f-016f-4edf-8e31-4ed7058eaeac";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setHTTPMethod:#"GET"];
[request setURL:[NSURL URLWithString:targetUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:
^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
NSString *myString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Data received: %#", myString);
}] resume];
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
[task setTaskCompletedWithSuccess:YES];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
#try {
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:nil];
}
#catch(NSException *e){
NSLog(#" Unable to submit request");
}
}
#end
Is background fetch broken in iOS 13? Even clicking on the “Simulate background fetch" in Xcode debug menu does not work. It just closes the app and nothing happens. Can anybody help/give any advice?
A few observations:
The setTaskCompletedWithSuccess should be inside the network request’s completion handler. You don’t want to mark the task as complete until the request has had a chance to run and you’ve processed the result.
You are calling submitTaskRequest, but passing nil for the NSError reference. You have also wrapped that in an exception handler. But this API call doesn’t throw exceptions, but rather just passes back errors. But you have to supply it an error reference. E.g.
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
NSError *error;
if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) {
NSLog(#"BGTaskScheduler failed: %#", error);
}
In your code, if it failed, you would never know.
You have placed this code in applicationDidEnterBackground. I.e., are you seeing this “Entering background” message at all? The reason I ask is that if you’ve supplied a scene delegate (common if you just created a new iOS 13 app), this method won’t be called, whereas sceneDidEnterBackground will.
You said that you tried “Simulate background fetch”. But you haven’t created a background fetch request (a BGAppRefreshTask). You created a background task (a BGProcessingTask), which is a different thing. To test background processing requests, refer to Starting and Terminating Tasks During Development.
There’s an interesting question as to how you know that the fetch request was processed. You’re just using NSLog (which presumes that you’re keeping your app attached to the Xcode debugger). I would suggest testing this without the app being attached to Xcode. There are a few options:
If you can watch your server logs for requests, that works.
I personally will often put in UserNotifications (and make sure to go into settings and turn on persistent notifications so I don’t miss them).
Another approach that I’ve done is to log these events in some table in my app’s persistent storage and then have some UI within the app to fetch this data so I can confirm what happened.
I’ll often use Unified Logging so that I can watch os_log statements issued by my device from the macOS Console even when Xcode is not running. This is very useful in logging app/scene methods. See WWDC 2016 Unified Logging and Activity Tracing
Whatever you do, for things like background processing, background app refresh, etc., I will program some mechanism so that I can check to see if the requests/tasks took place, even when not attached to Xcode. Being attached to the debugger can, in some cases, affect the app lifecycle, and I want to make sure I’ve got some way to confirm what was going on without the benefit of the console.
Likely obvious, but make sure you never “force quit” the app, as that will stop background processes from taking place.
For more information, See WWDC 2019 video Advances in App Background Execution.

Download Multiple Images Sequentially using NSURLSession downloadTask in Objective C

My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
The original approach was to just download all of them - but we realized that gave a lot of NSURLErrorTimedOut errors and crashed our program. We then implemented it such that we download all of the images, but in batches of 100 images at a time. Someone on SO suggested we actually implement our download like this:
Create a list of all file URLs that need to be downloaded.
Write your code so that it downloads these URLs sequentially. I.e. do
not let it start downloading a file until the previous one has
finished (or failed and you decided to skip it for now).
Use NSURLSession's support for downloading an individual file to a
folder, don't use the code to get an NSData and save the file
yourself. That way, your application doesn't need to be running while
the download finishes.
Ensure that you can tell whether a file has already been downloaded or
not, in case your download gets interrupted, or the phone is restarted
in mid-download. You can e.g. do this by comparing their names (if
they are unique enough), or saving a note to a plist that lets you
match a downloaded file to the URL where it came from, or whatever
constitutes an identifying characteristic in your case.
At startup, check whether all files are there. If not, put the missing
ones in above download list and download them sequentially, as in #2.
Before you start downloading anything (and that includes downloading
the next file after the previous download has finished or failed), do
a reachability check using the Reachability API from Apple's
SystemConfiguration.framework. That will tell you whether the user has
a connection at all, and whether you're on WiFi or cellular (in
general, you do not want to download a large number of files via
cellular, most cellular connections are metered).
We create a list of all images to download here:
- (void)generateImageURLList:(BOOL)batchDownloadImagesFromServer
{
NSError* error;
NSFetchRequest* leafletURLRequest = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription* leafletURLDescription = [NSEntityDescription entityForName:#"LeafletURL" inManagedObjectContext:managedObjectContext];
[leafletURLRequest setEntity:leafletURLDescription];
numberOfImages = [managedObjectContext countForFetchRequest:leafletURLRequest error:&error];
NSPredicate* thumbnailPredicate = [NSPredicate predicateWithFormat:#"thumbnailLocation like %#", kLocationServer];
[leafletURLRequest setPredicate:thumbnailPredicate];
self.uncachedThumbnailArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
NSPredicate* hiResPredicate = [NSPredicate predicateWithFormat:#"hiResImageLocation != %#", kLocationCache];
[leafletURLRequest setPredicate:hiResPredicate];
self.uncachedHiResImageArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
}
We use NSURLSession to download an individual image to a folder by calling hitServerForUrl and implementing didFinishDownloadingToURL:
- (void)hitServerForUrl:(NSURL*)requestUrl {
NSURLSessionConfiguration *defaultConfigurationObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigurationObject delegate:self delegateQueue: nil];
NSURLSessionDownloadTask *fileDownloadTask = [defaultSession downloadTaskWithURL:requestUrl];
[fileDownloadTask resume];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
if (isThumbnail)
{
leafletURL.thumbnailLocation = kLocationCache;
}
else
{
leafletURL.hiResImageLocation = kLocationCache;
}
// Filename to write to
NSString* filePath = [leafletURL pathForImageAtLocation:kLocationCache isThumbnail:isThumbnail isRetina:NO];
// If it's a retina image, append the "#2x"
if (isRetina_) {
filePath = [filePath stringByReplacingOccurrencesOfString:#".jpg" withString:#"#2x.jpg"];
}
NSString* dir = [filePath stringByDeletingLastPathComponent];
[managedObjectContext save:nil];
NSError* error;
[[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
NSURL *documentURL = [NSURL fileURLWithPath:filePath];
NSLog(#"file path : %#", filePath);
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
//Remove the old file from directory
}
[[NSFileManager defaultManager] moveItemAtURL:location
toURL:documentURL
error:&error];
if (error){
//Handle error here
}
}
This code calls loadImage, which calls `hitServer:
-(void)downloadImagesFromServer{
[self generateImageURLList:NO];
[leafletImageLoaderQueue removeAllObjects];
numberOfHiResImageLeft = [uncachedHiResImageArray count];
for ( LeafletURL* aLeafletURL in uncachedHiResImageArray)
{
//// Do the same thing again, except set isThumb = NO. ////
LeafletImageLoader* hiResImageLoader = [[LeafletImageLoader alloc] initWithDelegate:self];
[leafletImageLoaderQueue addObject:hiResImageLoader]; // do this before making connection!! //
[hiResImageLoader loadImage:aLeafletURL isThumbnail:NO isBatchDownload:YES];
//// Adding object to array already retains it, so it's safe to release it here. ////
[hiResImageLoader release];
uncachedHiResIndex++;
NSLog(#"uncached hi res index: %ld, un cached hi res image array size: %lu", (long)uncachedHiResIndex, (unsigned long)[uncachedHiResImageArray count]);
}
}
- (void)loadImage:(LeafletURL*)leafletURLInput isThumbnail:(BOOL)isThumbnailInput isBatchDownload:(BOOL)isBatchDownload isRetina:(BOOL)isRetina
{
isRetina_ = isRetina;
if (mConnection)
{
[mConnection cancel];
[mConnection release];
mConnection = nil;
}
if (mImageData)
{
[mImageData release];
mImageData = nil;
}
self.leafletURL = leafletURLInput;
self.isThumbnail = isThumbnailInput;
NSString* location = (self.isThumbnail) ?leafletURL.thumbnailLocation :leafletURL.hiResImageLocation;
//// Check if the image needs to be downloaded from server. If it is a batch download, then override the local resources////
if ( ([location isEqualToString:kLocationServer] || (isBatchDownload && [location isEqualToString:kLocationResource])) && self.leafletURL.rawURL != nil )
{
//NSLog(#"final loadimage called server");
//// tell the delegate to get ride of the old image while waiting. ////
if([delegate respondsToSelector:#selector(leafletImageLoaderWillBeginLoadingImage:)])
{
[delegate leafletImageLoaderWillBeginLoadingImage:self];
}
mImageData = [[NSMutableData alloc] init];
NSURL* url = [NSURL URLWithString:[leafletURL pathForImageOnServerUsingThumbnail:self.isThumbnail isRetina:isRetina]];
[self hitServerForUrl:url];
}
//// if not, tell the delegate that the image is already cached. ////
else
{
if([delegate respondsToSelector:#selector(leafletImageLoaderDidFinishLoadingImage:)])
{
[delegate leafletImageLoaderDidFinishLoadingImage:self];
}
}
}
Currently, I'm trying to figure out how to download the images sequentially, such that we don't call hitServer until the last image is finished downloading. Do I need to be downloading in the background? Thank you for suggestions!
My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
This seems like a job for on-demand resources. Just turn these files into on-demand resources obtained from your own server, and let the system take care of downloading them in its own sweet time.
This sounds very much like an architectural issue. If you fire off downloads without limiting them of course you're going to start getting timeouts and other things. Think about other apps and what they do. Apps that give the user the ability to do multiple downloads often limit how may can occur at once. iTunes for example can queue up thousands of downloads, but only runs 3 at a time. Limiting to just one at a time will only slow things down for your users. You need a balance that consider your user's available bandwidth.
The other part of this is to again consider what your users want. Does every one of your uses want every single image? I don't know what you are offering them, but in most apps which access resources like images or music, it's up to the user what and when they download. Thus they only download what they are interested in. So I'd recommend only downloading what the users are viewing or have somehow requested they want to download.

iOS-WatchKit File Transfers Work Unreliably

I've built an app for iOS 9 and WatchOS 2. The iOS app will periodically transfer image files from the iPhone to the Watch. Sometimes, these are pushed from the app, sometimes the Watch requests (pulls) them. If pulled, I make the requests asynchronous, and use the exact same iOS code to transfer images in both cases.
About half the time (maybe 2/3), the file transfer works. The other times, it appears that nothing happens. This is the same whether I'm pushing or pulling images.
On the iOS side, I use code similar to this (session activated already):
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
if (session.reachable) {
NSData *imgData = UIImagePNGRepresentation(img);
NSURL *tempFile = [[session watchDirectoryURL] URLByAppendingPathComponent: #"camera.png"];
BOOL success = [imgData writeToFile: [tempFile path] atomically: NO];
if (success) {
NSLog(#"transferFile:metadata:");
[session transferFile: tempFile metadata: nil];
} else {
NSLog(#"will not call transferFile:metadata:");
}
} else {
NSLog(#"Camera watch client not reachable.");
}
}
On the watch extension side, I have a singleton that activates the watch session and receives the file:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
// pass the data file to the data listener (if any)
[self.dataListener session: session didReceiveFile: file];
}
My "data listener" converts the file to a UIImage and displays it on the UI thread. However, that's probably irrelevant, as the unsuccessful operations never get that far.
During unsuccessful transfers, session:didReceiveFile: is never called. If I inspect the iOS app's log, however, I see these messages only during the operations that fail:
Dec 26 15:10:47 hostname companionappd[74893]: (Note ) WatchKit:
application (com.mycompany.MyApp.watchkitapp), install status: 2,
message: application install success
Dec 26 15:10:47 hostname
companionappd[74893]: (Note ) WatchKit: Purging
com.mycompany.MyApp.watchkitapp from installation queue, 0 apps
remaining
What is happening here? It looks like the app is trying to reinstall the Watch app (?). When this is happening, I do not see the watch app crash/close and restart. It simply does nothing. No file received.
On the iOS side, I scale down the image to about 136x170 px, so the PNG files shouldn't be too big.
Any ideas what's going wrong?
Update:
I have posted a complete, minimal project that demonstrates the problem on Github here
I am now under the impression that this is a bug in the simulators. It seems to work more reliably on the Apple Watch hardware. Not sure if it's 100% reliable, though.
Apple bug report filed (#24023088). Will update status if there is any, and leave unsolved for any potential answers that may provide workarounds.
For me, not a single transfer was working anymore. Polling transfer.progress showed isTransferring == true, but I never got beyond 0 completed units.
I ended up:
Deleting apps on watch and iPhone
Rebooting both
Reinstalling
And it works.
This is how I managed to transfer files from phone to watch:
In order for this to work, the file must be locate in appGroupFolder, and "App Groups" must be enabled from Capabilities tab, for phone and watch.
In order to get appGroup folder use following line of code:
NSURL * myFileLocationFolder = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: #"myGroupID"]; //something like group.bundle.projName
Once you got that use this to send message and handle response from watch:
[session sendMessage:#{#"file":myFileURL.absoluteString} replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//got reply
} errorHandler:^(NSError * _Nonnull error) {
//got Error
}];
Even though WCSession *session = [WCSession defaultSession]; I have noticed that sometimes session is deallocated, so you might consider using [WCSession defaultSession]; instead.
To catch this on the phone use:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
//message[#"file"] - addres to my file
//do stuff with it here
replyHandler(#{#"myResponse":#"responseData"}); //this call triggers replyHandler block on the watch
}
Now a if you didn't forget to implement WCSessionDelegate and use
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
_session.delegate = self;
[_session activateSession];
}
//here session is #property (strong, nonatomic) WCSession * session;
It all should work.
Made a broader answer, hopefully will reach out to more people.

Chaining background NSURLSession uploads

Has anyone been successful in chaining NSURLSession background uploads?
I am trying to upload a huge video file in 5 MB parts using background upload of NSURLSession. The uploads has to be in order. The whole thing works fine in foreground. I am using AFNetwoking for this, and its a multi part upload. But when the app is in background, the first item uploads fine and starts the second one in background (in setDidFinishEventsForBackgroundURLSessionBlock of AFURLSessionManager). But it stops abruptly (my best guess is in 30 seconds, as an app woken up in background has a max lifetime of 30 sec) and then nothing happens. I expected the second session will finish in background and call up the third etc - a chain behaviour, but this just does not seem to work.
I have tried adding all file parts to a single NSURLSession in one go with a HTTPMaximumConnectionsPerHost = 1 - this works fine and uploads the full file in parts. But the file parts are picked in random order, i.e. part 1 gets uploaded, then part 5, part 3, part 10 etc …. I tried adding this in an NSOperationQueue with dependency between operations and this seems to mess up the entire thing - the upload does not work at all.
I know that the video file can be uploaded as a single file in background, but the server expects this in 5 MB parts. Hence I guess my only option is to chain uploads, or add all the parts to a NSURLSession, but make sure they are always uploaded in the order they are added.
Any help would be appreciated.
My code:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[NSString stringWithFormat:#"%d", rand()]];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
config.HTTPMaximumConnectionsPerHost = 1;
[manager setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
dispatch_async(dispatch_get_main_queue(), ^{
// Call the completion handler to tell the system that there are no other background transfers.
// completionHandler();
[self upload];
});
}];
}
- (IBAction)start:(id)sender {
[self upload];
}
-(void) upload {
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Sample" ofType:#"mp4"];
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys:#"234", #"u", #"Sample.mp4", #"f",nil];
NSMutableURLRequest *request = [serializer multipartFormRequestWithMethod:#"POST" URLString:urlString parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:[NSURL fileURLWithPath:filePath] name:#"data" fileName:#"Sample.mp4" mimeType:#"video/mp4" error:nil];
} error:nil];
__block NSString *tempMultipartFile = [NSTemporaryDirectory() stringByAppendingPathComponent:#"Test"];
tempMultipartFile = [tempMultipartFile stringByAppendingString:[NSString stringWithFormat:#"%d", rand()]];
NSURL *filePathtemp = [NSURL fileURLWithPath:tempMultipartFile];
__block NSProgress *progress = nil;
[serializer requestWithMultipartFormRequest:request writingStreamContentsToFile:filePathtemp completionHandler:^(NSError *error) {
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePathtemp progress:&progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
NSLog(#"Request--> %#.\n Response --> %# \n%#", request.URL.absoluteString ,responseObject, error? [NSString stringWithFormat:#" with error: %#", [error localizedDescription]] : #""); //Lets us know the result including failures
[[NSFileManager defaultManager] removeItemAtPath:tempMultipartFile error:nil];
}];
[uploadTask resume];
[manager setTaskDidSendBodyDataBlock:^(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
NSLog(#"uploading");
}];
}];
}
Well, finally I reached out to Apple for clarifications on chaining background uploads - This is not possible in iOS.NSURLSession has a Resume Rate Limiter which prevents apps from executing chained tasks in background as explained in https://forums.developer.apple.com/thread/14854. Instead, apple suggests batch transfers or other options like https://forums.developer.apple.com/thread/14853. The other thing I was asking was to order the multiple tasks in upload queue - i.e, force NSURLSession to upload tasks in the order in which they are added. As pointed by dgatwood, using an NSOperationQueue is not possible and Apple also mentioned the same.As mentioned by eskimo in response mail "NSURLSession does not guarantee to run your requests in order and there’s no way to enforce that." So I am pretty much left option less on my original problem.
An NSOperationQueue goes away when your app does, which isn't too long after you put it into the background. So that's not going to work very well.
Instead, store a list of files remaining to upload, in order—either in a file on disk or in NSUserDefaults, depending on your personal preference. Then, use an upload task in a background session to start the first task. When it finishes, if your app isn't running, it should automatically get relaunched in the background to handle the data.
To support this behavior, in your application:handleEventsForBackgroundURLSession:completionHandler: method, re-create the background session just like you did originally, and store the completion handler.
Shortly thereafter, your delegate methods for the request should be called just as though your app were still running when the download finished. Among other things those methods can provide your app with the response data from the server, the response object (for checking the status code), etc.
When you get the didCompleteWithError delegate call (which is nil on success, IIRC), if the transfer failed, try it again or whatever. If it succeeded, start uploading the next one and update your list of files on disk.
Either way, when your session delegate's ** URLSessionDidFinishEventsForBackgroundURLSession:** method is called, call the handler you stored earlier, roughly like this:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
storedHandler();
}];
By calling the completion handler, you're telling the OS that you don't need to keep running.
Rinse, repeat.
If your app is still running when the request completes, everything happens just as described above, except that you don't get the application:handleEventsForBackgroundURLSession:completionHandler: or URLSessionDidFinishEventsForBackgroundURLSession: calls, which means you don't have to store the completion handler or call it.
See URL Session Programming Guide for details.

NSURLDomainErrorDomain error -999 when app terminate with NSURLSession

I have big trouble with NSURLSession when i'll terminate the App.
I have downloaded the apple sample:
https://developer.apple.com/library/ios/samplecode/SimpleBackgroundTransfer/Introduction/Intro.html
on Apple reference.
When i start download the file download correctly.
When i enter in background the download continues to.
When i terminate the application and i restart the app the application enter in:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
And i catch this error:
The operation couldn't be completed. (NSURLErrorDomain error -999.)
It seems that i cannot restore download when app has been terminated. It's correct?For proceed with download i must leave application active in background?
Thank you
Andrea
A couple of observations:
Error -999 is kCFURLErrorCancelled.
If you are using NSURLSessionDownloadTask, you can download those in the background using background session configuration, e.g.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundIdentifier];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
If not using background session (e.g. you have to use data task, for example), you can use beginBackgroundTaskWithExpirationHandler to request a little time for the app the finish requests in the background before the app terminates.
Note, when using background sessions, your app delegate must respond to handleEventsForBackgroundURLSession, capturing the completion handler that it will call when appropriate (e.g., generally in URLSessionDidFinishEventsForBackgroundURLSession).
How did you "terminate the app"? If you manually kill it (by double tapping on home button, holding down on icon for running app, and then hitting the little red "x"), that will not only terminate the app, but it will stop background sessions, too. Alternatively, if the app crashes or if it is simply jettisoned because foreground apps needed more memory, the background session will continue.
Personally, whenever I want to test background operation after app terminates, I have code in my app to crash (deference nil pointer, like Apple did in their WWDC video introduction to NSURLSession). Clearly you'd never do that in a production app, but it's hard to simulate the app being jettisoned due to memory constraints, so deliberately crashing is a fine proxy for that scenario.
i insert this new lines of code:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
BLog();
NSInteger errorReasonNum = [[error.userInfo objectForKey:#"NSURLErrorBackgroundTaskCancelledReasonKey"] integerValue];
if([error.userInfo objectForKey:#"NSURLErrorBackgroundTaskCancelledReasonKey"] &&
(errorReasonNum == NSURLErrorCancelledReasonUserForceQuitApplication ||
errorReasonNum == NSURLErrorCancelledReasonBackgroundUpdatesDisabled))
{
NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
if (resumeData) {
// resume
NSURL *downloadURL = [NSURL URLWithString:DownloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
if (!self.downloadTask) {
self.downloadTask = [self.session downloadTaskWithRequest:request];
}
[self.downloadTask resume];
if (!_session){
[[_session downloadTaskWithResumeData:resumeData]resume];
}
}
}
}
It catch NSURLErrorCancelledReasonUserForceQuitApplication but when the application try to [[_session downloadTaskWithResumeData:resumeData]resume]
reenter again in:
(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
and give me again -999 error.
I use this configuration
- (NSURLSession *)backgroundSession
{
/*
Using disptach_once here ensures that multiple background sessions with the same identifier are not created in this instance of the application. If you want to support multiple background sessions within a single process, you should create each session with its own identifier.
*/
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
let me explain what i mean with "terminate the app" (in ios8):
double tap on home button
swipe on my open app.
app disappear from open app list
relaunch app.
When i reopen the app i enter into callback with error
The operation couldn't be completed. (NSURLErrorDomain error -999.)
There is something that i can't understand. This behaviour make me crazy! :-(

Resources