ios - Cordova : sharing browser URL to my iOS app (Clipper ios share extension)

标签 ios iphone safari cfbundledocumenttypes

我要什么

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

Pocket example

有可能这样做吗?特别是 Cordova ?

最佳答案

编辑 :迟早一个简单的移动网站可能能够接收从 native 应用程序共享的内容。检查 Web Share Target协议(protocol)

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

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

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

您有 2 个选择:

  • 版本您的一些 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 Controller 中检索该页面上所需的数据(这里我想要 url 和标题)。

    自定义 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>
    

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

    自定义 ShareViewController.swift

    通常,您必须自己实现将在现有应用程序之上运行的 Swift View (对我而言是在浏览器应用程序之上)。

    默认情况下, Controller 将提供您可以使用的默认 View ,您可以从那里向后端执行请求。 Here is an example从中我激励自己这样做。

    但就我而言,我不是 iOS 开发人员,我希望当用户选择我的扩展程序时,它会打开我的应用程序而不是显示 iOS View 。所以我用了 custom URL scheme打开我的应用剪刀:myAppScheme://openClipper?url=SomeUrl这允许我在 HTML/JS 中设计我的剪刀,而不必创建 iOS View 。

    请注意,我为此使用了 hack,Apple 可能会禁止在 future 的 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 和标题。

    自动化流程

    您必须创建共享扩展文件和 Action.js文件通过 XCode 接口(interface)。然而,一旦它们被创建(并在 XCode 中被引用),你就可以用你自己的文件替换它们。

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

    这并不理想,但我们使用的最低程序是:
  • 构建 Cordova iOS 平台 ( cordova prepare ios )
  • 在 XCode 中打开项目
  • 使用(产品名称=“MyClipper”,语言=“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

    关于ios - Cordova : sharing browser URL to my iOS app (Clipper ios share extension),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33105698/

    相关文章:

    ios - 如何从 Swift 中访问 CLLocationManager?

    iphone - 如何使用 NSXMLParser 读取此 CDATA

    javascript - 运算符优先级 : ! 并等待

    debugging - Safari Web 检查器不断断开连接

    css - IE 和 Safari 中的 Flexbox 最大高度

    ios - 异步函数返回正常,但数组变为空

    ios - SwiftyJSON 添加 JSON 对象数组

    iphone - NavigationBar 图层渐变效果不佳

    iphone - 如何修复 iOS 11 搜索栏在选择时跳转到屏幕顶部 (Swift)?

    iphone - 与 iPhone 相比,iPad 上的通用应用程序使用的内存少得多