When you develop a Share Extension for your iOS App many times the Extension is not suitable for all places where it could be shown to the user. For example if your Share Extension can only share URLs it does not make sense to offer it to the user when he is using the Photos App.
This is why Apple implemented the NSExtensionActivationRule field in the Extension’s Info.plist. That’s quite straightforward and not exactly rocket science, but still many tutorials on Extension do not mention this and it took me a while to find out how this works, so I’ll share this with you.
So, when you create a new Extension Xcode creates a Info.plist in the ‘YOUR_EXTENSION/Supporting Files’ folder. In there you’ll find a Dictionary with the key NSExtension.
The interesting part is the String with the key NSExtensionActivationRule. By default Xcode sets its value to TRUEPREDICATE. That’s just for development purposes and makes sure that your Extension is shown as a sharing option everywhere. This has to be removed before you submit your app the the AppStore of your app will be rejected.
So in our case we can only share an URL so we want to make sure that it does not get added to the sharing options in Apps that do not offer an URL to share (like the Photos App).
So you have to change the type of the NSExtensionActivationRule field from String to Dictionary and add one or more keys that Apple provides to define what data types your Extension supports:
In our case we only support URLs so we need the NSExtensionActivationSupportsWebURLWithMaxCount key. So the NSExtension Dictionary should look like this:
And now your Share Extension is not activated in the Photo App anymore. Only in apps that offer a URL will now show your Share Extension as one of the sharing options.
Creating a Share Extension in iOS8 is really easy:
In your app choose File > New > Target
Choose Application Extension > Share Extension
Enter a name for your Share Extension and press Finish
That’s all! Xcode creates everything you need for you and puts it in it’s own folder. You’ll find a ShareViewController that is a subclass of SLComposeServiceViewController. That class’ UI looks a lot like the SLComposeViewController that you’ve probably worked with when using iOS’s Social Framework (to share your app’s content to Twitter or Facebook).
In ShareViewController Xcode creates some stubs for you where you have add the code to actually share the content to your service. In my case I want to build an extension for Safari that allows the user to share the page’s URL on my service. So when the user selects my share extension from Safari I need to retrieve the page’s URL to send it to my server.
When the user starts my Share Extension and presses the “Post” button the method didSelectPost is called on the ShareViewController.
Let’s have a look at the stub method that Xcode created:
- (void)didSelectPost {
// This is called after the user selects Post.
// Do the upload of contentText and/or NSExtensionContext attachments.
// 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.
[self.extensionContext completeRequestReturningItems:@[]
completionHandler:nil];
}
So far so good. So I added the code to retrieve the page URL. Compared to the easiness of creating the whole extension, the simple task of retrieving the URL takes quite some effort:
- (void)didSelectPost {
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"]) {
[itemProvider loadItemForTypeIdentifier:@"public.url"
options:nil
completionHandler:^(NSURL *url, NSError *error) {
NSString *urlString = url.absoluteString;
// send url to server to share the link
// BUT THIS BLOCK IS NEVER EXECUTED!!!
}];
}
[self.extensionContext completeRequestReturningItems:@[]
completionHandler:nil];
}
That’s a lot of code for just an URL. And the worst thing: It’s not working! The completion handler of the loadItemForTypeIdentifier:options:completionHandler: is never called.
As it turns out calling completeRequestReturningItems:completionHandler: at the bottom of the method causes this problem. In Apple’s documentation it says that you are supposed to call this method to ‘Tell the host app to complete the app extension request with an array of result items’. Calling this methods dismisses the ShareViewController and deallocates it. So the NSItemProvider that contains the URL is also destroyed before it can access the URL.
So the solution is quite simple: The call to completeRequestReturningItems:completionHandler: has to be done AFTER retrieving the URL and sending it to the server. So it has to go into the completionHandler block:
- (void)didSelectPost {
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"]) {
[itemProvider loadItemForTypeIdentifier:@"public.url"
options:nil
completionHandler:^(NSURL *url, NSError *error) {
NSString *urlString = url.absoluteString;
// send url to server to share the link
[self.extensionContext completeRequestReturningItems:@[]
completionHandler:nil];
}];
}
}
And now the URL is retrieved and can be posted to my server.
In case you need to do this in Swift, here is how that would look like:
override func didSelectPost() {
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
if let itemProvider = item.attachments?.first as? NSItemProvider {
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItemForTypeIdentifier("public.url", options: nil, completionHandler: { (url, error) -> Void in
if let shareURL = url as? NSURL {
// send url to server to share the link
}
self.extensionContext?.completeRequestReturningItems([], completionHandler:nil)
})
}
}
}
}
Do you have UIAlertViews that look strange under iOS8? The alert text is bold and clings to the top of the alert container?
Apparently iOS8 does not tolerate setting the UIAlertView’s title to nil. When you set it to an empty string instead, the UIAlertView looks like it should.
The funny thing is that with an UIAlertSheet the behavior is opposite: passing an empty string as title results in an empty title section on top of the UIActionSheet. Passing nil for the title fixes this.
After switching to Xcode 6 and the iOS8 SDK I was confronted with a strange error message when trying to test my app on a device. The app was built and deployed to the device but Xcode did not run it. Instead it showed an alert with the following error message:
Could not launch "APP_NAME", process launch failed: Security
The only thing you can do is to press “OK”, but the app still does not run. When you try to run it again from Xcode you just get the same alert.
The solution to this problem is quite simple:
Run the App from Xcode
When the alert shows, press “OK”
Open the app on the device
An alert will be shown, asking you if you want to trust this app. Press “Trust”.
That was quite useful when some feature in your app could not be used, because the user disabled something in his settings. In that case you could inform the user that he had to change his settings if he wanted to use that feature and offer to open the settings directly from your app.
Unfortunately Apple decided to not longer allow this after the introduction of iOS 5.1
Good news: This feature is back with iOS8! Apple has added a NSString constant to UIKit that you are supposed to use. To avoid crashes on iOS versions below iOS8 it is a good idea to check if the constant exists on the system version the device is running. You could check if the user is running iOS8 or higher but I prefer to check directly if the constant exits rather than checking the iOS version. Who knows if Apple will decide to remove this constant again in the future…
So, to open the settings, you can use the following snippet:
if let settingsURL = NSURL(string: UIApplicationOpenSettingsURLString) {
UIApplication.sharedApplication().openURL(settingsURL)
}
If your app has it’s own settings bundle, the settings will be opened showing your app’s settings. If your app does not have a setting bundle, the main settings page will be shown.
Unfortunately there does not seem to be a way to go directly to subpages of the settings (except your own settings bundle). If you’ve found a way to do that, please let me know in the comments!
Update: Linking to a subpage of the Setting seems to work again for system versions greater or equal than iOS8. It is not officially documented by Apple but it seems to work, although I not sure how reliable this is. It works on my iOS 9.1 iPhone but not in the Simulator. So I would not recommend to use this in production.
So, with caution, you can try this to open the Keyboard Settings:
if let settingsURL = NSURL(string: "prefs:root=General&path=Keyboard") {
UIApplication.sharedApplication().openURL(settingsURL)
}
Or go even one level deeper:
if let settingsURL = NSURL(string: "prefs:root=General&path=Keyboard/KEYBOARDS") {
UIApplication.sharedApplication().openURL(settingsURL)
}
To make these unofficial URLs work you might have to add prefs to the URL schemes of your project’s Info.plist file.