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

Posted

技术标签:

【中文标题】Cordova:将浏览器 URL 共享到我的 iOS 应用程序(Clipper ios 共享扩展)【英文标题】:Cordova: sharing browser URL to my iOS app (Clipper ios share extension) 【发布时间】:2015-10-13 14:44:45 【问题描述】:

我想要什么

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

有可能做到吗?尤其是 Cordova?

【问题讨论】:

嘿塞巴斯蒂安,成功了吗? @BurhanMughal 我已经回答了我自己的问题!这是可能的,我花了很长时间才弄清楚!希望对你有帮助 【参考方案1】:

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

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

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

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

你有两个选择:

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

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

手动创建共享扩展

非常重要:通过 XCode 界面创建共享扩展和Action.js!它们必须在 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 文件来描述您正在创建什么样的共享扩展,以及您可以将什么样的内容共享到您的应用程序。就我而言,我主要想分享 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>

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

自定义 ShareViewController.swift

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

默认情况下,控制器将提供一个您可以使用的默认视图,并且您可以从那里向您的后端执行请求。 Here is an example 我从中启发了自己这样做。

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

请注意,我为此使用了 hack,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://***.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://***.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&lt;String,String&gt;。这是因为 Chrome 和 Safari 似乎以两种不同的方式提供页面的 url 和标题。

自动化流程

您必须通过 XCode 界面创建共享扩展文件和Action.js 文件。但是,一旦它们被创建(并在 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-extension的内容复制到cordova/platforms/ios/MyClipper

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

Edit 2017:使用 cordova-ios@5.0.0 设置所有这些可能会变得更容易,请参阅 https://issues.apache.org/jira/browse/CB-10218

【讨论】:

谢谢!很有帮助! 非常感谢,这是 SO 上唯一一篇与原始问题相关的有用信息。我需要以各种组合处理 URL+文本+图像,并为此创建了此答案的派生。在没有合适的 iOS 共享扩展插件的情况下,github.com/inshikos/cordova-ios-share-extension/blob/master/… 可能会帮助其他需要将内容共享到 Cordova 应用程序的解决方案。 感谢@WarrenKim,您的代码将来也会对我有所帮助:) 嘿@SebastienLorber!你的帖子似乎真的很有帮助:) 谢谢!我想问一件事:我做了你描述的所有事情,但在共享屏幕中仍然看不到我的应用程序的图标。你能指出它可能卡在哪里吗? @StanislavIegorov 我不知道。也许你没有在 XCode 中设置 target >= 7.0,或者错过了一步【参考方案2】:

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

【讨论】:

如何做到这一点“上面的 doOpenUrl() 需要更新才能在 iOS 10 上运行。”?粘贴这些代码后,我有很多红点。【参考方案3】:

使用此cordova plugin,您应该能够以更少的手动工作来实现您的目标。它也适用于 android

【讨论】:

将您的插件与电容器 3 一起使用,它适用于图像和视频。但我无法从任何应用程序中获得text / URL。我的具体目标是从 Instagram 获取 Post URL。【参考方案4】:

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

我尝试使用 Jean-Christophe Hoelt 的出色 cordova-plugin-openwith,但遇到了几个问题。该插件旨在接收在安装期间配置的一种类型的共享项目(例如,URL、文本或图像)。此外,在当前的实现中,在 Cordova 应用程序中编写要共享的便笺和选择接收器是不同(本机和 Cordova)上下文中的两个不同步骤,因此对我来说这不是一个好的用户体验。

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

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

【讨论】:

【参考方案5】:

跟进 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 中创建扩展

    在扩展文件夹中选择 ShareViewController.swift 转到编辑 > 转换 > 到当前的 Swift 语法 在扩展构建设置中,将“仅需要应用扩展安全 API”切换为否。

只有这样扩展才能工作。

【讨论】:

使用此解决方案,它可以在模拟器上运行,但不能在真实设备上运行。知道为什么吗? 无法仅根据此评论告诉您原因,但您可以尝试在 Safari 中调试 javascript:确保您的手机已插入并且 Safari 已启用开发者模式(首选项 -> 高级)。当您启动该应用程序时,您的手机将出现在 Safari 的开发人员菜单中,您可以检查 web 视图并在那里检查控制台是否有错误。希望你能得到相关信息。检查 Xcode 中的日志,它们也可以包含相关信息。还要确保扩展名已正确签名。 感谢您的回复。不幸的是,应用程序中似乎一切正常(safari/xcode 日志中没有错误),此外,我的自定义 url 从 safari 启动应用程序,但不是从图库。我继续调查! 好的,这并不奇怪。到目前为止,我们只处理了从 Safari 中打开 URL,因为太忙而没有时间做更多的工作。如果您了解如何从其他应用程序处理它们,我很想听听如何!

以上是关于Cordova:将浏览器 URL 共享到我的 iOS 应用程序(Clipper ios 共享扩展)的主要内容,如果未能解决你的问题,请参考以下文章

iOS:我可以将 Safari 中的 URL 共享到我自己的应用程序吗?

如何从另一个应用程序将图像共享到我的 Cordova/PhoneGap 应用程序?

应用程序扩展 iOS9:如何将字符串 / URL 从共享视图发送到我的应用程序?

如何将外部 URL 加载到我的 webview 中?

iOS 共享扩展未显示 URL 和文本

Branch.io Cordova SDK 不生成链接