我想要什么
在Iphone上,访问Safari或Chrome中的网站时,可以将内容分享给其他应用。在这种情况下,您可以看到我可以将内容(基本上是URL)共享到名为Pocket的应用程序。
有可能吗?特别是Cordova?
答案 0 :(得分:23)
修改:简单的移动网站迟早可能会收到来自原生应用的内容。检查Web Share Target协议
我正在回答我自己的问题,因为我们最终成功为Cordova应用程序实现了iOS Share Extension。
首先,Share Extension系统仅适用于iOS> = 8
然而,将它集成到Cordova项目中会有点痛苦,因为没有特殊的Cordova配置。创建共享扩展时,Cordova团队很难对XCode xproj文件进行反向工程以添加共享扩展,因此将来可能会很难...
您有两个选择:
我们决定使用第二个选项,因为我们的扩展非常稳定,我们不会经常修改它。
非常重要:创建共享扩展,然后Action.js
通过XCode界面!它们必须在xproj文件中注册,否则根本不起作用。 See
要为Cordova应用创建共享扩展程序,您必须执行任何iOS developer would do。
您在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
文件,以描述您要创建的共享扩展程序类型,以及可以与应用程序共享的内容类型。在我的情况下,我主要想分享网址,所以这里的配置适用于从Chrome或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>
请注意,我们在该plist文件中注册了Action.js
文件。
通常你必须自己实现Swift视图,这些视图将在现有应用程序之上运行(对我来说,在浏览器应用程序之上)。
默认情况下,控制器将提供您可以使用的默认视图,您可以从那里对后端执行请求。 Here is an example我鼓励自己这样做。
但在我的情况下,我不是iOS开发人员,我希望当用户选择我的扩展时,它会打开我的应用而不是显示iOS视图。所以我使用了custom URL scheme来打开我的app剪辑器:myAppScheme://openClipper?url=SomeUrl
这允许我用HTML / JS设计我的剪辑器,而不必创建iOS视图。
请注意,我使用黑客攻击,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似乎以两种不同的方式提供网页的网址和标题。
您必须通过XCode界面创建共享扩展文件和Action.js
文件。但是,一旦创建它们(并在XCode中引用),您就可以用自己的文件替换它们。
因此我们决定在文件夹(/cordova/ios-share-extension
)中对上述文件进行版本控制,并使用它们覆盖默认的共享扩展文件。
这不是理想的,但我们使用的最小程序是:
cordova prepare ios
)/cordova/ios-share-extension
的内容复制到cordova/platforms/ios/MyClipper
这样,扩展程序在xproj文件中正确注册,但您仍然可以对扩展程序进行版本控制。
编辑2017 :使用cordova-ios@5.0.0设置所有内容可能会更容易,请参阅https://issues.apache.org/jira/browse/CB-10218
答案 1 :(得分:3)
doOpenUrl()需要更新才能在iOS 10上运行。以下代码也适用于旧版本的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
}
}
答案 2 :(得分:2)
使用此cordova plugin,您应该能够以更少的手动工作来实现目标。它也适用于Android。
答案 3 :(得分:1)
这是一个很好且仍然相关的问题。
我试图利用Jean-Christophe Hoelt的精彩 cordova-plugin-openwith ,但遇到了几个问题。该插件用于接收在安装期间配置的一种类型的共享项(例如,URL,文本或图像)。此外,通过其当前实现,在Cordova应用程序中编写共享和选择接收器的注释是不同(本机和Cordova)上下文中的两个不同步骤,因此它对我来说并不是一个良好的用户体验。 / p>
我对此插件进行了这些和其他更正,并将其作为单独的插件发布: https://github.com/EternallLight/cordova-plugin-openwith-ios
请注意,它仅适用于iOS,不适用于Android。
答案 4 :(得分:0)
关注Aaron Rosen的iOS 10更新评论,以下是让它发挥作用的过程:
在Sebastien Lorber的原始答案的代码中,按照Aaron的建议更新doOpenUrl函数。为清晰起见,请在此处重新发布:
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
}
}
按照初始答案中列出的流程在Xcode
只有这样才能延期。