Подтвердить что ты не робот

Кордова: обмен URL-адресом браузера с моим приложением для iOS (расширение share с расширением iOS)

Что я хочу

На Iphone при посещении веб-сайта внутри Safari или Chrome можно обмениваться контентом с другими приложениями. В этом случае вы можете увидеть, что я могу поделиться содержимым (в основном URL) с приложением Pocket.

Карманный пример

Можно ли это сделать? И конкретно с Кордовой?

4b9b3361

Ответ 1

Изменить: рано или поздно простой мобильный веб-сайт, вероятно, сможет получать контент, распространенный из собственных приложений. Проверьте Протокол веб-рассылки

Я отвечаю на свой вопрос, поскольку мы, наконец, преуспели в реализации расширения для iOS для приложения Cordova.

Сначала система расширения общего доступа доступна только для iOS >= 8

Однако это очень болезненно, чтобы интегрировать его в проект Cordova, потому что для этого нет специальной конфигурации Cordova. Когда вы создаете расширение Share, команде Cordova сложно перепрограммировать xproj файл XCode, чтобы добавить расширение общего доступа, поэтому, вероятно, это будет трудно и в будущем...

У вас есть 2 варианта:

  • Версии некоторых файлов вашей платформы iOS (например, файл xproj)
  • Включите ручную процедуру после создания платформы iOS с помощью кордовы

Мы решили пойти со вторым вариантом, так как наше расширение довольно стабильно, и мы не будем его часто изменять.

Создайте расширение общего доступа вручную

ОЧЕНЬ ВАЖНО: создайте расширение общего доступа и Action.js ПОСМОТРЕТЬ интерфейс XCode! Они должны быть зарегистрированы в файле xproj или вообще не работают. Смотрите

Создайте файлы через XCode

Чтобы создать расширение для приложения Кордовы, вам нужно будет сделать что-то вроде разработчик iOS.

  • Откройте платформу ios xproj на XCode
  • Файл > Создать > Целевое средство > Расширение Share
  • Выберите 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 (здесь я хочу URL и название).

Настроить Info.plist

Теперь вам нужно настроить файл Info.plist, чтобы описать, какое расширение расширения вы создаете, и какой контент вы можете поделиться с вашим приложением. В моем случае я в основном хочу разделить URL-адреса, так что вот конфигурация, которая работает для обмена URL-адресами из 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>

Обратите внимание, что мы зарегистрировали файл Action.js в этом файле plist.

Настроить ShareViewController.swift

Обычно вам придется реализовать сами представления Swift, которые будут выполняться поверх существующего приложения (для меня поверх приложения браузера).

По умолчанию контроллер предоставит представление по умолчанию, которое вы можете использовать, и оттуда вы можете выполнять запросы на ваш сервер. Вот пример, из которого я вдохновил себя на это.

Но в моем случае я не разработчик iOS, и я хочу, чтобы при выборе пользователем моего расширения он открыл мое приложение вместо отображения iOS-представлений. Поэтому я использовал схему настраиваемый URL-адрес, чтобы открыть клип-приложение для приложения: myAppScheme://openClipper?url=SomeUrl Это позволяет мне создавать мой клипер в HTML/JS вместо того, чтобы создавать iOS-представления.

Обратите внимание, что я использую взломать это, и Apple может запретить вам открывать приложение из расширения Share в будущих версиях iOS. Однако этот хак работает в настоящее время для iOS 8.x и 9.0.

Вот код. Он работает как для Chrome, так и для Safari на iOS.

//
//  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)
        })
    }
}

Обратите внимание, что есть 2 способа загрузки Dictionary<String,String>. Это связано с тем, что Chrome и Safari, по-видимому, предоставляют URL-адрес и заголовок страницы двумя разными способами.

Автоматизация процесса

Вы должны создать файлы расширения и файлы Action.js через интерфейс XCode. Однако, как только они создаются (и упоминаются в XCode), вы можете заменить их собственными файлами.

Итак, мы решили, что мы будем использовать эти файлы в папке (/cordova/ios-share-extension) и переопределить файлы расширения по умолчанию с ними.

Это не идеально, но минимальная процедура, которую мы используем:

  • Платформа сборки платформы Кордовы (cordova prepare ios)
  • Открыть проект в XCode
  • Создать расширение общего доступа с помощью (product name= "MyClipper", язык = "Swift", организация name= "MyCompany" )
  • В "MyClipper" создайте пустой файл "Action.js"
  • Скопируйте содержимое /cordova/ios-share-extension в cordova/platforms/ios/MyClipper

Таким образом, расширение правильно зарегистрировано в файле xproj, но у вас все еще есть возможность управлять версией вашего расширения.

Изменить 2017: это может стать проще настроить все с помощью [email protected], см. https://issues.apache.org/jira/browse/CB-10218

Ответ 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
    }
}

Ответ 3

Вы должны быть в состоянии достичь своей цели с гораздо меньшей ручной работой, используя этот плагин cordova. Он также будет работать на Android.

Ответ 4

Следуя рекомендациям по обновлению iOS 10 Аароном Розеном, здесь процесс, чтобы заставить его работать:

  • В коде исходного ответа Себастьяна Лорбера обновите функцию 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 в папке расширения
  • Перейдите в меню "Редактирование" > "Преобразовать" > "Текущий синтаксис"
  • В настройках сборки расширений переключится на "Требовать только API для расширения приложений и расширений" до NO.

Только тогда будет работать расширение.

Ответ 5

Это хороший и все еще актуальный вопрос.

Я попытался использовать удивительную кордовую плагин-openwith от Jean-Christophe Hoelt, но столкнулся с несколькими проблемами. Плагин предназначен для приема общих элементов одного типа (например, URL, текст или изображение), который настроен во время установки. Кроме того, с его текущей реализацией написание заметки для совместного использования и выбора получателя в приложении Cordova - это два разных шага в разных (родной и кордовой) контекстах, поэтому для меня это не выглядело как хороший пользовательский интерфейс.

Я внес эти и другие исправления в этот плагин и опубликовал его как отдельный плагин: https://github.com/EternallLight/cordova-plugin-openwith-ios

Обратите внимание, что он работает только для iOS, а не для Android.