编辑:一个简单的移动网站迟早可能会接收到本地应用程序共享的内容。检查Web Share Target协议
我正在回答我自己的问题,因为我们终于成功地为 Cordova 应用程序实现了 iOS 共享扩展。
首先共享扩展系统仅适用于 iOS >= 8
但是,将它集成到 Cordova 项目中有点痛苦,因为没有特殊的 Cordova 配置可以这样做。在创建共享扩展时,Cordova 团队很难对 XCode xproj 文件进行逆向工程以添加共享扩展,因此将来可能也很难......
你有两个选择:
- 版本化您的一些 iOS 平台文件(如 xproj 文件)
- 在使用 cordova 生成 iOS 平台后包含一个手动过程
我们决定使用第二个选项,因为我们的扩展非常稳定,我们不会经常修改它。
手动创建共享扩展
非常重要:通过 XCode 界面创建共享扩展和Action.js!它们必须在 xproj 文件中注册,否则根本不起作用。 See
通过 XCode 创建文件
要为 Cordova 应用程序创建共享扩展,您必须像任何iOS developer would do 一样。
- 在XCode上打开ios平台xproj
- 文件 > 新建 > 目标 > 共享扩展
- 选择 Swift 作为语言(只是因为 ObjC 对我来说似乎不愉快)
您会在 XCode 中获得一个新文件夹,其中包含一些您必须自定义的文件。
您还需要该共享扩展文件夹中的额外Action.js 文件。创建一个新的空文件(通过 XCode!)Action.js
处理浏览器数据提取
输入Action.js以下代码:
var Action = function() {};
Action.prototype = {
run: function(parameters) {
parameters.completionFunction({"url": document.URL, "title": document.title });
},
finalize: function(parameters) {
}
};
var ExtensionPreprocessingJS = new Action
当您在浏览器顶部选择共享扩展时(我认为它仅适用于 Safari),此 JS 将运行并允许您在 Swift 控制器中检索该页面上所需的数据(这里我想要网址和标题)。
自定义 Info.plist
现在您需要自定义Info.plist 文件来描述您正在创建什么样的共享扩展,以及您可以将什么样的内容共享到您的应用程序。就我而言,我主要想分享 url,所以这里有一个配置,可用于从 Chrome 或 Safari 分享 url。
<?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>
请注意,我们在该 plist 文件中注册了 Action.js 文件。
自定义 ShareViewController.swift
通常您必须自己实现 Swift 视图,这些视图将在现有应用程序之上运行(对我而言,在浏览器应用程序之上)。
默认情况下,控制器将提供一个您可以使用的默认视图,并且您可以从那里向您的后端执行请求。 Here is an example 我从中启发了自己这样做。
但就我而言,我不是 iOS 开发人员,我希望当用户选择我的扩展程序时,它会打开我的应用程序而不是显示 iOS 视图。所以我使用custom URL scheme 打开我的应用剪辑器:myAppScheme://openClipper?url=SomeUrl
这允许我用 HTML / JS 设计我的剪辑器,而不必创建 iOS 视图。
请注意,我为此使用了 hack,Apple 可能会禁止在未来的 iOS 版本中从共享扩展程序打开您的应用程序。但是,此 hack 目前适用于 iOS 8.x 和 9.0。
这里是代码。它适用于 iOS 上的 Chrome 和 Safari。
//
// 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)
})
}
}
请注意,有两种方法可以加载 Dictionary<String,String>。这是因为 Chrome 和 Safari 似乎以两种不同的方式提供页面的 url 和标题。
自动化流程
您必须通过 XCode 界面创建共享扩展文件和Action.js 文件。但是,一旦它们被创建(并在 XCode 中引用),您就可以将它们替换为您自己的文件。
因此我们决定将上述文件版本化到一个文件夹 (/cordova/ios-share-extension) 中,并用它们覆盖默认的共享扩展文件。
这并不理想,但我们使用的最小程序是:
- 构建 Cordova iOS 平台 (
cordova prepare ios)
- 在 XCode 中打开项目
- 使用 (product name="MyClipper", language="Swift", organization name="MyCompany") 创建共享扩展
- 在“MyClipper”上,创建一个空文件“Action.js”
- 将
/cordova/ios-share-extension的内容复制到cordova/platforms/ios/MyClipper
这样,扩展在 xproj 文件中正确注册,但您仍然可以对扩展进行版本控制。
Edit 2017:使用 cordova-ios@5.0.0 设置所有这些可能会变得更容易,请参阅 https://issues.apache.org/jira/browse/CB-10218