What I want
On an Iphone, when visiting a website inside Safari or Chrome, it is possible to share content to other apps. In this case, you can see I can share the content (basically the URL) to an app called Pocket.
Is it possible to do that? And specifically with Cordova?
Safari for iOS is using WebKit2 (starting from iOS 8). Details about WebKit2. This seems to answer to your question. This plugin makes Cordova use the WKWebView component instead of the default UIWebView component, and is installable only on a system with the iOS 9.0 SDK.
Share Extension is an easy way that Apple provides to share contents (images, audio, files, etc.) from one app to another, even made by different developers.
You can use cordova run ios --list to see all available targets and cordova run ios --target=target_name to run application on a specific device or emulator (for example, cordova run ios --target="iPhone-6" ). You can also use cordova run --help to see additional build and run options.
To submit apps to the Apple App Store℠ requires the latest versions of the Apple tools. You can test many of the Cordova features using the iOS simulator installed with the iOS SDK and Xcode, but you need an actual device to fully test all of the app's device features before submitting to the App Store.
Edit: sooner or later a simple mobile website will probably be able to receive content shared from native apps. Check the Web Share Target protocol
I'm answering my own question, as we finally succeeded implementing the iOS Share Extension for a Cordova application.
First the Share Extension system is only available for iOS >= 8
However it is kind of painful to integrate it in a Cordova project because there's no special Cordova config to do so. When creating a Share Extension, it is hard for the Cordova team to reverse-engineer the XCode xproj file to add a share extension so it will probably be hard in the future too...
You have 2 options:
We decided to go with the 2nd option, as our extension is pretty stable and we will not modify it often.
VERY IMPORTANT: create the share extension, and the Action.js
THROUGH the XCode interface! They have to be registered in the xproj file or it won't work at all. See
To create a share extension for a Cordova app, you will have to do like any iOS developer would do.
You get a new folder in XCode with some files that you will have to customize.
You will also need an extra Action.js
file in that share extension folder. Create a new empty file (through XCode!) Action.js
Put in Action.js
the following code:
var Action = function() {};
Action.prototype = {
run: function(parameters) {
parameters.completionFunction({"url": document.URL, "title": document.title });
},
finalize: function(parameters) {
}
};
var ExtensionPreprocessingJS = new Action
When your share extension is selected on top of a browser (I think it only works for Safari), this JS will be run and will permit you to retrieve the data you want on that page in your Swift controller (here I want the url and the title).
Now you need to customize the Info.plist
file to describe what kind of share extension you are creating, and what kind of content you can share to your app. In my case I mostly want to share urls, so here is a config that works for sharing urls from Chrome or Safari.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>MyClipper</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>Action</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
Notice that we registered the Action.js
file in that plist file.
Normally you would have to implement by yourself Swift views that will be run on top of the existing app (for me on top of the browser app).
By default, the controller will provide a default view that you can use, and you can perform requests to your backend from there. Here is an example from which I inspired myself that do so.
But in my case, I am not an iOS developer and I want that when the user select my extension, it opens my app instead of displaying iOS views. So I used a custom URL scheme to open my app clipper: myAppScheme://openClipper?url=SomeUrl
This permits me to design my clipper in HTML / JS instead of having to create iOS views.
Notice that I use a hack for that, and Apple may forbid to open your app from a Share Extension in future iOS versions. However this hack works currently for iOS 8.x and 9.0.
Here is the code. It works for both Chrome and Safari on iOS.
//
// ShareViewController.swift
// MyClipper
//
// Created by Sébastien Lorber on 15/10/2015.
//
//
import UIKit
import Social
import MobileCoreServices
@available(iOSApplicationExtension 8.0, *)
class ShareViewController: SLComposeServiceViewController {
let contentTypeList = kUTTypePropertyList as String
let contentTypeTitle = "public.plain-text"
let contentTypeUrl = "public.url"
// We don't want to show the view actually
// as we directly open our app!
override func viewWillAppear(animated: Bool) {
self.view.hidden = true
self.cancel()
self.doClipping()
}
// We directly forward all the values retrieved from Action.js to our app
private func doClipping() {
self.loadJsExtensionValues { dict in
let url = "myAppScheme://mobileclipper?" + self.dictionaryToQueryString(dict)
self.doOpenUrl(url)
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
private func dictionaryToQueryString(dict: Dictionary<String,String>) -> String {
return dict.map({ entry in
let value = entry.1
let valueEncoded = value.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
return entry.0 + "=" + valueEncoded!
}).joinWithSeparator("&")
}
// See https://github.com/extendedmind/extendedmind/blob/master/frontend/cordova/app/platforms/ios/extmd-share/ShareViewController.swift
private func loadJsExtensionValues(f: Dictionary<String,String> -> Void) {
let content = extensionContext!.inputItems[0] as! NSExtensionItem
if (self.hasAttachmentOfType(content, contentType: contentTypeList)) {
self.loadJsDictionnary(content) { dict in
f(dict)
}
} else {
self.loadUTIDictionnary(content) { dict in
// 2 Items should be in dict to launch clipper opening : url and title.
if (dict.count==2) { f(dict) }
}
}
}
private func hasAttachmentOfType(content: NSExtensionItem,contentType: String) -> Bool {
for attachment in content.attachments as! [NSItemProvider] {
if attachment.hasItemConformingToTypeIdentifier(contentType) {
return true;
}
}
return false;
}
private func loadJsDictionnary(content: NSExtensionItem,f: Dictionary<String,String> -> Void) {
for attachment in content.attachments as! [NSItemProvider] {
if attachment.hasItemConformingToTypeIdentifier(contentTypeList) {
attachment.loadItemForTypeIdentifier(contentTypeList, options: nil) { data, error in
if ( error == nil && data != nil ) {
let jsDict = data as! NSDictionary
if let jsPreprocessingResults = jsDict[NSExtensionJavaScriptPreprocessingResultsKey] {
let values = jsPreprocessingResults as! Dictionary<String,String>
f(values)
}
}
}
}
}
}
private func loadUTIDictionnary(content: NSExtensionItem,f: Dictionary<String,String> -> Void) {
var dict = Dictionary<String, String>()
loadUTIString(content, utiKey: contentTypeUrl , handler: { url_NSSecureCoding in
let url_NSurl = url_NSSecureCoding as! NSURL
let url_String = url_NSurl.absoluteString as String
dict["url"] = url_String
f(dict)
})
loadUTIString(content, utiKey: contentTypeTitle, handler: { title_NSSecureCoding in
let title = title_NSSecureCoding as! String
dict["title"] = title
f(dict)
})
}
private func loadUTIString(content: NSExtensionItem,utiKey: String,handler: NSSecureCoding -> Void) {
for attachment in content.attachments as! [NSItemProvider] {
if attachment.hasItemConformingToTypeIdentifier(utiKey) {
attachment.loadItemForTypeIdentifier(utiKey, options: nil, completionHandler: { (data, error) -> Void in
if ( error == nil && data != nil ) {
handler(data!)
}
})
}
}
}
// See https://stackoverflow.com/a/28037297/82609
// Works fine for iOS 8.x and 9.0 but may not work anymore in the future :(
private func doOpenUrl(url: String) {
let urlNS = NSURL(string: url)!
var responder = self as UIResponder?
while (responder != nil){
if responder!.respondsToSelector(Selector("openURL:")) == true{
responder!.callSelector(Selector("openURL:"), object: urlNS, delay: 0)
}
responder = responder!.nextResponder()
}
}
}
// See https://stackoverflow.com/a/28037297/82609
extension NSObject {
func callSelector(selector: Selector, object: AnyObject?, delay: NSTimeInterval) {
let delay = delay * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
NSThread.detachNewThreadSelector(selector, toTarget:self, withObject: object)
})
}
}
Notice there are 2 ways to load the Dictionary<String,String>
. This is because Chrome and Safari seems to provide the url and title of the page in 2 different ways.
You must create the Share Extension files and Action.js
file through the XCode interface. However, once they are created (and referenced in XCode), you can replace them with your own files.
So we decided that we will version the above files in a folder (/cordova/ios-share-extension
), and override the default share extension files with them.
This is not ideal but the minimum procedure we use is:
cordova prepare ios
)/cordova/ios-share-extension
to cordova/platforms/ios/MyClipper
This way the extension is correctly registered in the xproj file but you still have the ability to version control your extension.
Edit 2017: this may become easier to setup all that with [email protected], see https://issues.apache.org/jira/browse/CB-10218
doOpenUrl() above needs to be updated to work on iOS 10. The following code also works on older versions of iOS.
private func doOpenUrl(url: String) {
let url = NSURL(string:url)
let context = NSExtensionContext()
context.open(url! as URL, completionHandler: nil)
var responder = self as UIResponder?
while (responder != nil){
if responder?.responds(to: Selector("openURL:")) == true{
responder?.perform(Selector("openURL:"), with: url)
}
responder = responder!.next
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With