Share extensions, introduced in iOS 8, give users an easy and convenient way to share content with other entities, such as social sharing websites or upload services. Previously, sharing content usually entailed switching from one app to another, for example, while surfing in Safari, if you wanted to share a URL, you would copy it, switch to the app you wanted to save or share it in, perform the action and then resume surfing in Safari. With share extensions, users will now be able to share content to your service direct from within the app they are using, be it Safari, Photos or other apps. This isn’t limited to system applications. Any custom application that presents an instance of the UIActivityViewController class will be able to see your sharing extension if you built your extension so that it can handle the file type provided by that application.
We are going to build a Share extension that shares photos to a social networking site. To make things simple we’ll use Imgur for this as it allows users to upload images anonymously (without the images being linked to an account).
Just like any other extension, a share extension cannot be a stand alone app; it must come bundled with a container app. I’ve created a starter project that will be our container app. Download it to follow along.
How the Demo App Works
The app is a simple application called ImgurShare that the user can use to upload images to Imgur. I’ll give the instructions of getting it ready to run shortly, but first let me give an overview of the app.
Its root view is a table view that lists the titles we give to the images uploaded to Imgur.
To upload an image, tap the + button in the navigation bar and you will be presented with a view which enables you to select an image, enter a title for the image and upload to Imgur.
After uploading the image, when you go back to the table view, the title of the image uploaded will be listed. When you select an item on the table view, you will be shown the image uploaded and its link on Imgur. To keep it simple, I did not include edit and delete functionality.
There is a Copy URL button on the navigation bar of this view, which you can use to copy the image’s URL to the clipboard. You can test it out by copying the URL, opening Safari and pasting in the link. You will see your uploaded image on Imgur.
Getting Started
With the app overview out of the way, we can now set it up. There are a couple things you need to do to get it running. First, you’ll need a Client ID from Imgur to use their API and second, you’ll need to set up an App Group. You need a developer account to enable App Groups. In order for an extension to share data with its container app, you need to configure an App Group. This will allow access to a shared container between the extension and container app.
We want the extension to have access to the shared container for two reasons: first, just like the container app, every image uploaded via the extension will be saved and made available for viewing via the container app and second, we use a background session for the upload which requires the image to be saved first before uploading, so we save a temporary image in the shared container which is used in the background upload task. We use background upload because users tend to get back to the host app immediately after finishing their task in extensions, so if the upload is potentially lengthy, we want it to run to completion even when the extension is terminated.
To get the Imgur client id, login to imgur.com or register an account at imgur.com/register. Once logged in, click your username at the top right of the screen and select Settings.
Then select Applications from the menu on the right.
Select create your own under Apps Used.
Click Register an Application from the menu on the left and select register their application as shown.
On the application form, enter whatever you want for the Application name. For Authorization type, select OAuth 2 authorization without a callback URL, enter your email address in Email and enter whatever you like for description. Enter the security text into the box and submit the form.
After a successful submission, you will be shown your Client ID and Client secret.
Copy the Client ID and open UploadImageService.swift (find it in the ImgurKit group) and paste it in the statement shown.
1 | private let imgurClientId = "IMGUR_CLIENT_ID" |
To configure the App Group, first change the app’s Bundle Identifier. Select ImgurShare in the Project Navigator, and then select the ImgurShare target from the targets list. On the General tab, change the Bundle identifier from com.appcoda.ImgurShare to something else. You need a different identifier because the identifier that you use for your app group has to match the Bundle identifier and it has to be unique.
Next go to the Capabilities tab and switch on the App Groups switch. Add a new group and name it group.com.[DOMAIN_CHOSEN_IN_PREVIOUS_INSTRUCTION].ImgurShare. For my case, I have group.com.appcoda.ImgurShare.
Creating the Share Extension
Open AddImageViewController.swift file and in the shareImage(imageTitle:, imageToUpload:) function, change the container identifier to match the group you created.
1 | config.sharedContainerIdentifier = "group.com.appcoda.ImgurShare" |
Open ImageService.swift and do the same for the following statement in the tempContainerURL(image:, name:) function.
1 | if let containerURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.com.appcoda.ImgurShare") { |
Do the same for the statement shown in the getFileUrl() function in the same class.
1 | if let containerUrl = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.com.appcoda.ImgurShare") { |
You can now run and test the app.
Next we’ll create the Share extension. Select the ImgurShare project in the Project Navigator and then go to Editor > Add Target > iOS > Application Extension > Share Extension
On the next screen set the product name to ImgurUpload and leave the remaining fields set to their default. Click on Finish and activate the ImgurUpload scheme
Next, configure the App Group for the ImgurUpload target. Select the ImgurShare project in the Project Navigator and then select the ImgurUpload target. On the Capabilities tab, turn on the App Groups switch and select the group you created previously for the ImgurShare target.
In order to enable code sharing between the extension and container app, I put the code in a framework. We need to link this to the ImgurUpload target.
With the ImgurUpload target selected, on the General tab, click the + button under the Linked Frameworks and Libraries section. Select ImgurKit.framework from the list and click Add.
With that set up, let’s now look at the files generated when we created the extension. Expand the ImgurUpload group and you will see a storyboard file, an entitlements file, a view controller and a plist file under the Supporting Files group. The entitlements file in an extension, just as in the container app, is generated when you configure App Groups. It holds details of your app group. Just as in Action Extensions, you can use a JavaScript file to get content from web pages in Safari. We won’t be using this in our extension, but for an idea on how to utilize JavaScript files to get data from the host app to the extension’s view controller, you can refer to the previous article we did on Action Extensions. The process and set up is quite similar.
You can use the storyboard file to create a custom interface (or do it in code), but Share extensions come with a default compose view which we’ll use. It’s interface is similar to the compose view you get when you share something to Twitter or Facebook.
We need to specify the type of content that our extension supports by setting its activation rule in the plist file. Open Info.plist and expand the NSExtension key. Then expand the NSExtensionAttributes key to get to the NSExtensionActivationRule. By default, this is set to TRUEPREDICATE which means that your extension will always be made available when the user wants to share content. You need to change this and make it specific if you want your app to be approved by the App Store. Change its type to Dictionary and add a new key-value pair. Set the key name to NSExtensionActivationSupportsImageWithMaxCount, type to Number and value to 1. This specifies that the extension supports sharing a single image at a time.
Looking at the ShareViewController class, you will find the following method stubs.
- isContentValid() – This is where user input is validated. You validate the text input as well as the content to be shared and return true if validation passes. The Post button on the compose view will remain disabled until true is returned.
- didSelectPost() – This is called after the user taps the Post button and it is here that you upload the content being shared. Once the upload is scheduled, you must call completeRequestReturningItems([], completionHandler:) so that the host app can un-block its UI. When the upload request is done, it calls the completion handler that was passed into the previous mentioned function call.
- configurationItems() – The default compose view of the share extension allows you to set what appears at the bottom of the view using table view cells. To do so, you must return an array of SLComposeSheetConfigurationItem objects if you have configurations for the user to set. Otherwise, return an empty array.
Add the following imports to the ShareViewController class.
1 2 | import ImgurKit import MobileCoreServices |
Add the following variable to the class. This will hold the image selected by the user.
1 | var selectedImage: UIImage? |
Next we’ll override viewDidLoad() to extract the image from the attachment items in extensionContext. When the user posts an image, it is bundled with other metadata and passed to the extension via the extensionContext object. Add the following to the class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | override func viewDidLoad() { super.viewDidLoad() let content = extensionContext!.inputItems[0] as NSExtensionItem let contentType = kUTTypeImage as String for attachment in content.attachments as [NSItemProvider] { if attachment.hasItemConformingToTypeIdentifier(contentType) { attachment.loadItemForTypeIdentifier(contentType, options: nil) { data, error in if error == nil { let url = data as NSURL if let imageData = NSData(contentsOfURL: url) { self.selectedImage = UIImage(data: imageData) } } else { let alert = UIAlertController(title: "Error", message: "Error loading image", preferredStyle: .Alert) let action = UIAlertAction(title: "Error", style: .Cancel) { _ in self.dismissViewControllerAnimated(true, completion: nil) } alert.addAction(action) self.presentViewController(alert, animated: true, completion: nil) } } } } } |
Here we extract the attachment item in the NSItemProvider object, then check to see if the attachment conforms to the type kUTTypeImage. kUTTypeImage is a system-defined string constant for core uniform type identifiers defined in the MobileCoreServices framework. We use it to identify attachment types.
The image is wrapped in the NSItemProvider class so we first need to load it using loadItemForTypeIdentifier(). If this is successful, we load the image from the url of where it is stored (when extracted from NSItemProvider, images are saved to the disk). We then set the selectedImage variable with the image. Incase of an error, the user will be shown an alert.
Next modify the isContentValid() function as shown.
1 2 3 4 5 6 7 8 9 | override func isContentValid() -> Bool { if let img = selectedImage{ if !contentText.isEmpty { return true } } return false } |
This check to make sure that an image is selected and the user types in some input before they can post.
Modify didSelectPost() as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | override func didSelectPost() { let defaultSession = UploadImageService.sharedService.session let defaultSessionConfig = defaultSession.configuration let defaultHeaders = defaultSessionConfig.HTTPAdditionalHeaders let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.appcoda.ImgurShare.bkgrdsession") config.sharedContainerIdentifier = "group.com.appcoda.ImgurShare" config.HTTPAdditionalHeaders = defaultHeaders let session = NSURLSession(configuration: config, delegate: UploadImageService.sharedService, delegateQueue: NSOperationQueue.mainQueue()) let completion: (TempImage?, NSError?, NSURL?) -> () = { image, error, tempURL in if error == nil { if let imageURL = image?.link { let image = Image(imgTitle: self.contentText, imgImage: self.selectedImage!) image.url = imageURL let imageService = ImageService.sharedService imageService.addImage(image) imageService.saveImages() } if let container = tempURL { var delError: NSError? if NSFileManager.defaultManager().isDeletableFileAtPath(container.path!) { let success = NSFileManager.defaultManager().removeItemAtPath(container.path!, error: &delError) if(!success) { println("Error removing file at path: \(error?.description)") } } } } else { println("Error uploading image: \(error!)") if let container = tempURL { var delError: NSError? if NSFileManager.defaultManager().isDeletableFileAtPath(container.path!) { let success = NSFileManager.defaultManager().removeItemAtPath(container.path!, error: &delError) if(!success) { println("Error removing file at path: \(error?.description)") } } } } } let title = contentText UploadImageService.sharedService.uploadImage(selectedImage!, title: title, session: session, completion:completion) self.extensionContext?.completeRequestReturningItems([], nil) } |
Replace the App Group identifier in the statement below with your own.
1 | config.sharedContainerIdentifier = "group.com.appcoda.ImgurShare" |
Here we define the background session we’ll use for the upload. We then define the completion block that will be called when the request completes. In the completion block, we check to see if the upload was successful and if so, save the image (images saved here will be loaded on the container app’s table view). We then delete the temporary image that had been saved to disk during the background request. If the upload failed, we also delete the temporary image and don’t save it for later viewing on the container app. We only want the container app to display images that were successfully uploaded to Imgur.
Note the use of contentText. This property holds the text input from the user.
We then call the uploadImage() function in the ImgurKit framework to do the upload. This method is asynchronous and will immediately return. When the upload completes or fails, the completion block will be called.
Next modify configurationItems() as shown. We return an empty array as we will not be adding anything to the extension’s compose view.
1 2 3 | override func configurationItems() -> [AnyObject]! { return [] } |
Testing the Share Extension
You can now run the app. Make sure the ImgurUpload scheme is selected. When prompted to choose an app to run, choose Photos. Once running, select a photo and tap on the Share button. On the first run, you will need to add your extension to the share sheet. Select the More button to the right of the other share icons and turn on the switch for your extension then select Done.
Back on the share sheet, you will be able to see your extension icon next to the other share icons. Select your extension, enter some text and tap on Post.
The image will be uploaded to Imgur and saved to the container app. To confirm this, navigate to the home screen and open ImgurShare. You will see the title you entered for the image and the image itself on the detail view.
If you leave the container app and navigate back to the Photos app to share another image, and go back to the container app, you will notice that the table doesn’t automatically update with added item. If you select a table row and then go back to the table view, the table will be updated. We need to reload the table data when the app comes back into the foreground. To do this first add the following function to ImagesTableViewController class.
1 2 3 | func refreshTable() { tableView.reloadData() } |
Then in AppDelegate.swift, add the following to the applicationWillEnterForeground() function.
1 2 | let vc = ImagesTableViewController() vc.refreshTable() |
Now whenever you upload an image via the extension and bring the container app back to the foreground, the table will be updated.
Summary
That concludes this Share Extension tutorial. I hope the guide comes in handy to anyone wanting to create a Share extension. You can download the project files here.
Note: When sharing a file via the extension, you will notice an error message in the debugger “Failed to inherit CoreMedia permissions from xxxx”. This seems to be a common error, and I have encountered it when working with Today, Share and action extensions. The extension still works fine despite the error. Searching around, it seems to be common and in this Stackoverflow post, someone says that their extension works fine and was approved by Apple despite getting the error. So, it might be an Xcode bug. I’m not sure. You can leave your thoughts in the comment.
Không có nhận xét nào:
Đăng nhận xét