Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Background upload with share extension

I created an macOS ShareExtension which I want to use to upload pictures.

I'm still testing this so any requests will be sent to https://beeceptor.com.

The share extension works fine and it shows up in Preview, once I run it:

the share extension

I add some text and hit "Post"

Creating the post

But the image is then not uploaded. This is my code that initiates the background upload:

let sc_uploadURL = "https://xyz.free.beeceptor.com/api/posts" // https://beeceptor.com/console/xyz

override func didSelectPost() {
    // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
    let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
    let sessionConfig = URLSessionConfiguration.background(withIdentifier: configName)
    // Extensions aren't allowed their own cache disk space. Need to share with application
    sessionConfig.sharedContainerIdentifier = "group.CreateDaily"
    let session = URLSession(configuration: sessionConfig)

    // Prepare the URL Request
    let request = urlRequestWithImage(image: attachedImage, text: contentText)

    // Create the task, and kick it off
    let task = session.dataTask(with: request! as URLRequest)
    task.resume()

    // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
    extensionContext?.completeRequest(returningItems: [AnyObject](), completionHandler: nil)
}

private func urlRequestWithImage(image: NSImage?, text: String) -> NSURLRequest? {
    let url = URL(string: sc_uploadURL)!
    let request: NSMutableURLRequest? =  NSMutableURLRequest(url: url as URL)
    request?.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request?.addValue("application/json", forHTTPHeaderField: "Accept")
    request?.httpMethod = "POST"

    let jsonObject = NSMutableDictionary()
    jsonObject["text"] = text
    if let image = image {
        jsonObject["image_details"] = extractDetailsFromImage(image: image)
    }

    // Create the JSON payload
    let jsonData = try! JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions.prettyPrinted)
    request?.httpBody = jsonData
    return request
}

Please note that the sharedContainerIdentifier is present in the entitlements of the app as well as in the sharing extensions entitlements.

shared-container

The ShareExtensions is in the respective App Group and has outgoing connections enabled.

app group and networking

like image 942
Besi Avatar asked Sep 29 '18 11:09

Besi


2 Answers

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:

  1. Go to the capabilities tab of the app’s target
  2. Enable App Groups
  3. Create a new app group, entitled something appropriate. It must start with group.. In the demo the group is called group.ShareAlike
  4. Let Xcode go through the process of creating this group for you.

enter image description here

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.

enter image description here

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:

enter image description here

like image 66
Kousic Avatar answered Nov 03 '22 05:11

Kousic


An App Group identifier must start with "group." and must match everywhere it is used - in the entitlements files, in your code, and on the Apple Dev portal.

In your app and share extension entitlement definitions, you have $(TeamIdentifierPrefix).group.CreateDaily. This is not valid, since it does not begin with "group.".

In your code, you just have "group.CreateDaily". This would be fine if it matched what was in your entitlement files, though Apple recommends using reverse domain name notation to avoid conflicts.

My recommendation would be to go to the Apple Dev portal under Certificates, Identifiers & Profiles/ Identifiers/ AppGroups and define your app groups. Apple will not let you enter something that does not begin with "group.". Once that has been setup, make sure that what you have in your entitlement files and code (config.sharedContainerIdentifier) match and then everything should work.

like image 36
Paul King Avatar answered Nov 03 '22 05:11

Paul King