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

Seb*_*ber 16 iphone safari ios cfbundledocumenttypes

我想要的是

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

口袋的例子

有可能吗?特别是Cordova?

Seb*_*ber 24

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

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

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

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

你有2个选择:

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

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

手动创建共享扩展

非常重要:创建共享扩展,以及Action.js通过XCode接口!它们必须在xproj文件中注册,否则根本不起作用.看到

通过XCode创建文件

要为Cordova应用程序创建共享扩展,您必须像iOS开发人员那样做.

  • 在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
Run Code Online (Sandbox Code Playgroud)

当您在浏览器上选择共享扩展程序时(我认为它仅适用于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>
Run Code Online (Sandbox Code Playgroud)

请注意,我们Action.js在该plist文件中注册了该文件.

自定义ShareViewController.swift

通常你必须自己实现Swift视图,这些视图将在现有应用程序之上运行(对我来说,在浏览器应用程序之上).

默认情况下,控制器将提供您可以使用的默认视图,您可以从那里对后端执行请求.这是一个我鼓励自己这样做的例子.

但在我的情况下,我不是iOS开发人员,我希望当用户选择我的扩展时,它会打开我的应用程序,而不是显示iOS视图.所以我使用自定义URL方案来打开我的app clipper: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)
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,有两种方法可以加载Dictionary<String,String>.这是因为Chrome和Safari似乎以两种不同的方式提供页面的网址和标题.

自动化流程

您必须Action.js通过XCode界面创建共享扩展文件和文件.但是,一旦创建它们(并在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-extensioncordova/platforms/ios/MyClipper

这样,扩展程序在xproj文件中正确注册,但您仍然可以对扩展程序进行版本控制.

编辑2017:使用cordova-ios@5.0.0可以更容易地设置所有这些,请参阅https://issues.apache.org/jira/browse/CB-10218