cordova:共享浏览器URL到我的iOS应用程序(Clipper iOS共享扩展)

我想要的是

在Iphone上,当访问Safari或Chrome内的网站时,可以将内容分享给其他应用程序。 在这种情况下,您可以看到我可以将内容(基本上是URL)共享到一个名为Pocket的应用程序。

口袋的例子

有没有可能做到这一点? 而具体与cordova?

编辑 :迟早一个简单的移动网站可能会接收从本机应用程序共享的内容。 检查Web共享目标协议

我正在回答我自己的问题,因为我们终于成功地为Cordova应用程序实现了iOS Share Extension。

首先,共享扩展系统仅适用于iOS> = 8

然而,将它集成到Cordova项目中是很痛苦的,因为没有特殊的Cordovaconfiguration。 创build共享扩展时,cordova团队很难对XCode xproj文件进行反向工程,以添加共享扩展,所以在将来可能也很难。

你有2个选项:

  • 版本的一些你的iOS平台文件(如xproj文件)
  • 在用cordova生成iOS平台之后,包含一个手动过程

我们决定去第二个选项,因为我们的扩展是相当稳定的,我们不会经常修改它。

手动创build共享扩展

非常重要 :创build共享扩展,以及通过XCode接口的Action.js ! 他们必须在xproj文件中注册,否则根本无法工作。 看到

通过XCode创build文件

要为Cordova应用创build共享扩展,您必须像iOS开发人员一样执行操作 。

  • 在XCode上打开ios平台xproj
  • 文件>新build>目标>共享分机
  • selectSwift作为语言(仅仅因为ObjC对我来说似乎不愉快)

您将在XCode中获得一个新文件夹,其中包含一些您必须自定义的文件。

您还需要在该共享扩展文件夹中添加一个额外的Action.js文件。 创build一个新的空文件(通过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 

当你在浏览器上select你的共享扩展(我认为它只适用于Safari)时,这个JS将会运行,并允许你在你的Swift控制器中检索你想要的数据(在这里我想要url和标题)。

自定义Info.plist

现在您需要自定义Info.plist文件来描述您正在创build什么样的共享扩展,以及您可以共享给您的应用程序的内容types。 在我的情况下,我主要是想分享url,所以这里是一个可用于从Chrome或Safari共享url的configuration。

 <?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视图,它将在现有的应用程序之上运行(在浏览器应用程序之上)。

默认情况下,控制器将提供一个可以使用的默认视图,您可以从这里执行对后端的请求。 这是我从中激发自己的一个例子 。

但在我的情况,我不是一个iOS开发人员,我希望当用户select我的扩展,它打开我的应用程序,而不是显示iOS视图。 所以我使用了一个自定义的URLscheme来打开我的应用程序剪辑器: myAppScheme://openClipper?url=SomeUrl这使我可以在HTML / JS中devise剪辑器,而不必创buildiOS视图。

请注意,我使用黑客,苹果可能会禁止在未来的iOS版本中从共享扩展中打开您的应用程序。 然而,这个黑客工程目前在iOS 8.x和9.0。

这是代码。 它适用于iOS和Chrome浏览器。

 // // 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界面创build共享扩展文件和Action.js文件。 但是,一旦它们被创build(并在XCode中引用),您可以用自己的文件replace。

所以我们决定将上面的文件放在一个文件夹( /cordova/ios-share-extension )中,并用它们覆盖默认的共享文件扩展名。

这并不理想,但我们使用的最低程序是:

  • build立cordovaiOS平台( cordova prepare ios
  • 在XCode中打开项目
  • 使用(产品名称=“MyClipper”,language =“Swift”,组织名称=“MyCompany”)创build共享扩展
  • 在“MyClipper”上,创build一个空文件“Action.js”
  • /cordova/ios-share-extension的内容复制到cordova/platforms/ios/MyClipper

通过这种方式,扩展程序在xproj文件中正确注册,但您仍然有能力对您的扩展进行版本控制。

编辑2017 :使用cordova-ios@5.0.0可能会更容易设置,请参阅https://issues.apache.org/jira/browse/CB-10218

上面的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 } } 

使用这个cordova插件,你应该能够用更less的手工工作来实现你的目标。 它也可以在Android上运行。

接下来是Aaron Rosen的iOS 10更新评论,下面是使其工作的过程:

  1. 在Sebastien Lorber的原始代码中,按照Aaron的build议更新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 } } 
  2. 按照初始答案中概述的过程在Xcode中创build扩展

  3. 在扩展文件夹中selectShareViewController.swift
  4. 转到编辑>转换>到当前的Swift语法
  5. 在扩展版本设置中,将“仅需要应用程序扩展安全API”切换到“否”。

只有这样才能扩展工作。

这是一个很好而且仍然相关的问题。

我试图利用由Jean-Christophe Hoelt创build的真棒cordova-plugin-openwith ,但遇到了几个问题。 该插件旨在接收在安装期间configuration的一种types(例如,URL,文本或图像)的共享项目。 另外,在目前的实现中,写一个笔记来分享和select一个Cordova应用程序中的接收器是不同的(本地和Cordova)上下文中的两个不同的步骤,因此它对我来说并不是一个好的用户体验。

我做了这些和其他更正这个插件,并作为一个单独的插件发布: https : //github.com/EternallLight/cordova-plugin-openwith-ios

请注意,它仅适用于iOS,不适用于Android。