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

时间:2015-10-13 14:44:45

标签: ios iphone safari cfbundledocumenttypes

我想要什么

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

Pocket example

有可能吗?特别是Cordova?

5 个答案:

答案 0 :(得分:23)

修改:简单的移动网站迟早可能会收到来自原生应用的内容。检查Web Share Target协议

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

首先,Share Extension系统仅适用于iOS> = 8

然而,将它集成到Cordova项目中会有点痛苦,因为没有特殊的Cordova配置。创建共享扩展时,Cordova团队很难对XCode xproj文件进行反向工程以添加共享扩展,因此将来可能会很难...

您有两个选择:

  • 版本化一些iOS平台文件(例如xproj文件)
  • 在使用cordova生成iOS平台后包含手动过程

我们决定使用第二个选项,因为我们的扩展非常稳定,我们不会经常修改它。

手动创建共享扩展程序

非常重要:创建共享扩展,然后Action.js通过XCode界面!它们必须在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文件,以描述您要创建的共享扩展程序类型,以及可以与应用程序共享的内容类型。在我的情况下,我主要想分享网址,所以这里的配置适用于从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文件。

自定义ShareViewController.swift

通常你必须自己实现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 iOS平台(cordova prepare ios
  • 在XCode中打开项目
  • 使用(产品名称=“MyClipper”,language =“Swift”,组织名称=“MyCompany”)创建共享扩展程序
  • 在“MyClipper”上,创建一个空文件“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

答案 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更新评论,以下是让它发挥作用的过程:

  1. 在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
    }
    }
    
  2. 按照初始答案中列出的流程在Xcode

  3. 中创建扩展程序
  4. 在扩展程序文件夹
  5. 中选择ShareViewController.swift
  6. 转到修改&gt;转换&gt;当前的Swift语法
  7. 在扩展程序构建设置中,切换&#34;仅要求App-Extension-Safe API&#34;没有。
  8. 只有这样才能延期。