Background upload with share extension
Performing a background upload
Once the user has completed their entry, and clicks the Post button, then the extension should upload the content to some web service somewhere. For the purposes of this example, the URL of the endpoint is contained within a property on the view controller:
let sc_uploadURL = "http://requestb.in/oha28noh"
This is a URL for the Request Bin service, which gives you a temporary URL to allow you to test network operations. The above URL (and the one in the sample code) won’t work for you, but if you visit requestb.in then you can get hold of your own URL for testing.
As mentioned previously, it’s important that extensions put very little strain on the limited system resources. Therefore, at the point the Post button is tapped, there is no time to perform a synchronous, foreground network operation. Luckily, NSURLSession
provides a simple API for creating background network operations, and that’s what you’ll need here.
The method which gets called when the user taps post is didSelectPost()
, and in its simplest form it must look like this:
override func didSelectPost() {
// Perform upload
...
// Inform the host that we're done, so it un-blocks its UI.
extensionContext?.completeRequestReturningItems(nil, completionHandler: nil)
}
Setting up an NSURLSession
is pretty standard:
let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName)
// Extensions aren't allowed their own cache disk space. Need to share with application
sessionConfig.sharedContainerIdentifier = "group.ShareAlike"
let session = NSURLSession(configuration: sessionConfig)
The important part to note of the above code segment is the line which sets the sharedContainerIdentifier on the session configuration. This specifies the name of the container that NSURLSession can use as a cache (since extensions don’t have their own writable disc access). This container needs to be set up as part of the host application (i.e. ShareAlike in this demo), and can be done through Xcode:
- Go to the capabilities tab of the app’s target
- Enable App Groups
- Create a new app group, entitled something appropriate. It must
start with group.. In the demo the group is called group.ShareAlike - Let Xcode go through the process of creating this group for you.
Then you need to go to the extension’s target, and follow the same process. Note that you won’t need to create a new app group, but instead select the one that you created for your host application.
These app groups are registered against your developer ID, and the signing process ensures that only your apps are able to access these shared containers.
Xcode will have created an entitlements file for each of your projects, and this will contain the name of the shared container it has access to.
Now that you’ve got your session set up correctly, you need to create a URL request to perform:
// Prepare the URL Request
let request = urlRequestWithImage(attachedImage, text: contentText)
This calls a method which constructs a URL request which uses HTTP POST to send some JSON, which includes the string content, and some metadata properties about the image:
func urlRequestWithImage(image: UIImage?, text: String) -> NSURLRequest? {
let url = NSURL.URLWithString(sc_uploadURL)
let request = NSMutableURLRequest(URL: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.HTTPMethod = "POST"
var jsonObject = NSMutableDictionary()
jsonObject["text"] = text
if let image = image {
jsonObject["image_details"] = extractDetailsFromImage(image)
}
// Create the JSON payload
var jsonError: NSError?
let jsonData = NSJSONSerialization.dataWithJSONObject(jsonObject, options: nil, error: &jsonError)
if jsonData {
request.HTTPBody = jsonData
} else {
if let error = jsonError {
println("JSON Error: \(error.localizedDescription)")
}
}
return request
}
This method doesn’t actually create a request which uploads the image, although it could be adapted to do so. Instead, it extracts some details about the image using the following method:
func extractDetailsFromImage(image: UIImage) -> NSDictionary {
var resultDict = [String : AnyObject]()
resultDict["height"] = image.size.height
resultDict["width"] = image.size.width
resultDict["orientation"] = image.imageOrientation.toRaw()
resultDict["scale"] = image.scale
resultDict["description"] = image.description
return resultDict
}
Finally, you can ask the session to create a task associated with the request you’ve built, and then call resume() on it to kick it off in the background:
// Create the task, and kick it off
let task = session.dataTaskWithRequest(request!)
task.resume()
If you run through this process now, with your own requestb.in URL in place, then you can expect to see results like this:
iOS Photo Upload From Share Extension, Respond When Upload Complete
Your NSURLSession must have a delegate assigned -preferably not the subclass of SLComposeServiceViewController
-, that object is the one that should take care of removing those files from the shared container folder -see containerURLForSecurityApplicationGroupIdentifier:
.
Your delegate must implement at least, -URLSession:task:didCompleteWithError:
at which point it finished with one task from the session. The task is associated with one file, so you must keep track which file is associated to, in order for you to delete it.
The delegate for the session will live until the task finishes, unless iOS terminates it for some reason, then you should implement -application:handleEventsForBackgroundURLSession:completionHandler:
in your AppDelegate, recreate the session to continue the uploading tasks and use the delegate to handle progress and completion of session.
Testing could be very tricky, so when testing, try to use big files to provoke iOS to terminate your Sharing Extension so your main app could take care, in the background, of the pending upload tasks.
Share Extension not uploading full size images
The problem is i am not using background session
NSURLSessionConfiguration *configuration= [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:configurationName];
- (void)performUploadWith3:(NSDictionary *)parameters imageFile:(UIImage *)image
{
NSString *fileName = @"image.file";
NSString *boundary= @"Boundary+2EB36F87257DBBD4";
NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kGroupNameToShareData];
storeURL=[storeURL URLByAppendingPathComponent:fileName];
NSLog(@"sotre url %@",storeURL);
NSData *imageData = UIImageJPEGRepresentation(image, 0.8);
imageData =[self appendParams:parameters andImage:image bondary:boundary];
if (!([imageData writeToFile:[storeURL path] atomically:YES]))
{
NSLog(@"Failed to save file.");
}
NSString *string=@"URL here";
NSURL *url = [NSURL URLWithString:string];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"POST"];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
for (NSString *key in parameters)
{
[request setValue:[parameters objectForKey:key] forHTTPHeaderField:key];
}
if (session == nil)
{
//If this is the first upload in this batch, set up a new session
//Each background session needs a unique ID, so get a random number
NSInteger randomNumber = arc4random() % 1000000;
sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[NSString stringWithFormat:@"and unique name"]];
sessionConfiguration.sharedContainerIdentifier=kGroupNameToShareData;
// config.HTTPMaximumConnectionsPerHost = 1;
session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
//Set the session ID in the sending message structure so we can retrieve it from the
//delegate methods later
// [sendingMessage setValue:session.configuration.identifier forKey:@"sessionId"];
NSLog(@"config %@",sessionConfiguration.sharedContainerIdentifier);
}
if([[NSFileManager defaultManager]fileExistsAtPath:[storeURL path]])
{
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:storeURL];
[uploadTask resume];
}
else
NSLog(@"file does not exsit");
NSLog(@"desc %@",[session sessionDescription]);
// NSLog(@"state %ld", [uploadTask state]);
}
Background task in iOS action extension
After many tests and failures, I found the following solution to execute a long performing task in the background of an extension. This works as expected, even if the extension is already finished:
func performTask()
{
// Perform the task in background.
let processinfo = ProcessInfo()
processinfo.performExpiringActivity(withReason: "Long task") { (expired) in
if (!expired) {
// Run task synchronously.
self.performLongTask()
}
else {
// Cancel task.
self.cancelLongTask()
}
}
}
This code uses ProcessInfo.performExpiringActivity()
to execute the task in another thread. It’s important that the task in performLongTask()
is executed synchronously. When the end of the block is reached, the thread will terminate and end the execution of your task.
A similar approach is also working in the main app. It's described in detail in a small summary for background tasks in iOS.
Uploading multiple images from Share Extension
I ran into the same problem recently and was able to resolve it by adding a counter and counting down as the images successfully completed their block. Within the loadItemForTypeIdentifier
completion block I then check to see if all items have been called before calling the completeRequestReturningItems
within a dispatch_once
block (just for safety's sake).
__block NSInteger imageCount;
static dispatch_once_t oncePredicate;
NSItemProvider *attachment = inputItem.attachments[0];
if ([attachment hasItemConformingToTypeIdentifier:(NSString*)kUTTypeImage])
{
[attachment loadItemForTypeIdentifier:(NSString*)kUTTypeImage options:nil completionHandler:^(NSData *item ,NSError *error)
{
if (item)
{
// do whatever you need to
imageCount --;
if(imageCount == 0){
dispatch_once(&oncePredicate, ^{
[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
});
}
}
}];
}
I can't say I feel like this is an overly elegant solution however, so if someone knows of a more appropriate way of handling this common use case I'd love to hear about it.
Related Topics
Command Failed Due to Signal: Segmentation Fault: 11 While Emitting Ir Sil Function
How to Trigger Updateuiview of a Uiviewrepresentable
Swift Generic Constraints in Init
How to Increase the Scope of Variables in Switch-Case/Loops in Swift
Rotating Uitextview Programmatically
Swift: Reduce Function with a Closure
Using Nil-Coalescing Operator with Try? for Function That Throws and Returns Optional
Arkit/Realitykit - People Occlusion Config Not Working
Swift 3 - Pass Struct by Reference via Unsafemutablerawpointer
Core Image Filter Cisourceovercompositing Not Appearing as Expected with Alpha Overlay
How to Detect Text View Begin Editing and End Editing in Swift 3
Send Mail with File Attachment
Phase 1 and Phase 2 Initialization in Swift
How to Get the Url from Webview in Swift
Swift - How to Resize a Uiview Based on Its Uilabel Size (That Is Inside)
Override Multiple Overloaded Init() Methods in Swift
Ar with iOS: Putting a Light in the Scene Makes Everything Black