简体   繁体   English

共享扩展以打开包含应用程序

[英]Share Extension to open containing app

I want to create an Android Style share feature for my app.我想为我的应用创建一个 Android 风格的共享功能。 I created a share extension which gets called when you select pictures inside the stock photo app and press share.我创建了一个共享扩展,当您在库存照片应用程序中选择图片并按下共享时,它会被调用。 Now I want those pictures to be sent to the main app and get handled over there.现在我希望将这些图片发送到主应用程序并在那里处理。 My question is now:我现在的问题是:

  1. Can iOS open my app after a button is pressed on the share extension window?在共享扩展窗口上按下按钮后,iOS 可以打开我的应用程序吗?
  2. How do I get the picture files inside my main app?如何在我的主应用程序中获取图片文件?

Swift 4+ (tested on iOS 13) Swift 4+(在 iOS 13 上测试)

@objc should be added to the declaration of openURL , that is, @objc应该添加到openURL的声明中,即

@objc func openURL(_ url: URL) -> Bool {
    // Code below.
}

Without it one would see this compiler error:没有它,人们会看到这个编译器错误:

Argument of '#selector' refers to instance method 'openURL' that is not exposed to Objective-C

Working solution in Swift 3.1 (tested in iOS10): Swift 3.1 中的工作解决方案(在 iOS10 中测试):

You need to create your own URL Scheme , then add this function to your ViewController and call it with openURL("myScheme://myIdentifier")您需要创建自己的 URL Scheme ,然后将此函数添加到您的 ViewController 并使用openURL("myScheme://myIdentifier")调用它

//  Function must be named exactly like this so a selector can be found by the compiler!
//  Anyway - it's another selector in another instance that would be "performed" instead.
func openURL(_ url: URL) -> Bool {
    var responder: UIResponder? = self
    while responder != nil {
        if let application = responder as? UIApplication {
            return application.perform(#selector(openURL(_:)), with: url) != nil
        }
        responder = responder?.next
    }
    return false
}

Edit: Notes for clarification: openURL is a method of UIApplication - since your ShareExtension is not derived from UIApplication I added my own openURL with the same definition as the one from UIApplication to keep the compiler happy (so that #selector(openURL(_:) can be found).编辑:澄清说明: openURL是 UIApplication 的一种方法 - 因为您的 ShareExtension 不是从 UIApplication 派生的,所以我添加了自己的openURL ,其定义与 UIApplication 中的定义相同,以使编译器满意(以便#selector(openURL(_: )可以找到)。

Then I go through the responders until I find one that is really derived from UIApplication and call openURL on that.然后我查看响应程序,直到我找到一个真正派生自UIApplication的响应程序并调用openURL

More stripped-down-example-code which copies files in a ShareExtension to a local directory, serializing filenames and calling openURL on another app:将 ShareExtension 中的文件复制到本地目录、序列化文件名并在另一个应用程序上调用 openURL 的更多精简示例代码:

//
//  ShareViewController.swift
//

import UIKit
import Social
import MobileCoreServices

class ShareViewController: UIViewController {

var docPath = ""

override func viewDidLoad() {
    super.viewDidLoad()

    let containerURL = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.com.my-domain")!
    docPath = "\(containerURL.path)/share"
    
    //  Create directory if not exists
    do {
        try FileManager.default.createDirectory(atPath: docPath, withIntermediateDirectories: true, attributes: nil)
    } catch let error as NSError {
        print("Could not create the directory \(error)")
    } catch {
        fatalError()
    }

    //  removing previous stored files
    let files = try! FileManager.default.contentsOfDirectory(atPath: docPath)
    for file in files {
        try? FileManager.default.removeItem(at: URL(fileURLWithPath: "\(docPath)/\(file)"))
    }
}

override func viewDidAppear(_ animated: Bool) {

    let alertView = UIAlertController(title: "Export", message: " ", preferredStyle: .alert)
    
    self.present(alertView, animated: true, completion: {

        let group = DispatchGroup()
        
        NSLog("inputItems: \(self.extensionContext!.inputItems.count)")
        
            for item: Any in self.extensionContext!.inputItems {
                
            let inputItem = item as! NSExtensionItem
            
            for provider: Any in inputItem.attachments! {
                
                let itemProvider = provider as! NSItemProvider
                group.enter()
                itemProvider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { data, error in
                    if error == nil {
                        //  Note: "data" may be another type (e.g. Data or UIImage). Casting to URL may fail. Better use switch-statement for other types.
                        //  "screenshot-tool" from iOS11 will give you an UIImage here
                        let url = data as! URL
                        let path = "\(self.docPath)/\(url.pathComponents.last ?? "")"
                        print(">>> sharepath: \(String(describing: url.path))")

                        try? FileManager.default.copyItem(at: url, to: URL(fileURLWithPath: path))
                        
                    } else {
                        NSLog("\(error)")
                    }
                    group.leave()
                }
            }
        }
        
        group.notify(queue: DispatchQueue.main) {
            NSLog("done")
            
            let files = try! FileManager.default.contentsOfDirectory(atPath: self.docPath)
            
            NSLog("directory: \(files)")
            
            //  Serialize filenames, call openURL:
            do {
                let jsonData : Data = try JSONSerialization.data(
                    withJSONObject: [
                        "action" : "incoming-files"
                        ],
                    options: JSONSerialization.WritingOptions.init(rawValue: 0))
                let jsonString = (NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
                let result = self.openURL(URL(string: "myapp://com.myapp.share?\(jsonString!)")!)
            } catch {
                alertView.message = "Error: \(error.localizedDescription)"
            }
            self.dismiss(animated: false) {
                self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
            }
        }
    })
}

//  Function must be named exactly like this so a selector can be found by the compiler!
//  Anyway - it's another selector in another instance that would be "performed" instead.
@objc func openURL(_ url: URL) -> Bool {
    var responder: UIResponder? = self
    while responder != nil {
        if let application = responder as? UIApplication {
            return application.perform(#selector(openURL(_:)), with: url) != nil
        }
        responder = responder?.next
    }
    return false
}
}

Currently there's no way to do this.目前没有办法做到这一点。 A share extension cannot open the containing app.共享扩展程序无法打开包含的应用程序。

The intended approach for share extensions is that they handle all of the necessary work themselves.共享扩展的预期方法是它们自己处理所有必要的工作。 Extensions can share code with their containing apps by using custom frameworks, so in most cases that's no problem.扩展可以使用自定义框架与其包含的应用程序共享代码,因此在大多数情况下这没有问题。

If you want to make data available to your app, you can set up an app group so that you have a shared directory.如果您想让数据对您的应用程序可用,您可以设置一个应用程序组,以便您拥有一个共享目录。 The extension can write data there, and the app can read it.扩展程序可以在那里写入数据,应用程序可以读取它。 That won't happen until the next time the user launches the app, though.但是,直到用户下次启动应用程序时才会发生这种情况。

Technically you can't open containing app from share extension, but you can schedule local notification, and that's what I end up doing.从技术上讲,您无法从共享扩展中打开包含应用程序,但您可以安排本地通知,这就是我最终要做的。 Just before I call super.didSelectPost, I schedule local notification with some text, and if user wants to open containing app, they can, and if not - they can continue with their workflow.就在我调用 super.didSelectPost 之前,我用一些文本安排本地通知,如果用户想要打开包含应用程序,他们可以,如果不是 - 他们可以继续他们的工作流程。 I even think its a better approach than automatically opening containing app and disrupting what they are doing.我什至认为这是比自动打开包含应用程序并破坏他们正在做的事情更好的方法。

I opened the host app from shared extension with a trick.我用一个技巧从共享扩展中打开了主机应用程序。 Using a webview with clear background color.使用具有清晰背景颜色的 webview。 below is the code下面是代码

 NSString *customURL = @"MY_HOST_URL_SCHEME_APP://";
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)];
webView.backgroundColor = [UIColor clearColor];
    webView.tintColor = [UIColor clearColor];
    [webView setOpaque:NO];
    [self.view addSubview:webView];
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:customURL]];
    [webView loadRequest:urlRequest];
    [self didSelectCancel];

Implement custom url schema in host app and call openURL(url:) method在主机应用程序中实现自定义 url 架构并调用 openURL(url:) 方法

like openURL(url:NSURL(string:"schema_name://"))像 openURL(url:NSURL(string:"schema_name://"))

extension SLComposeServiceViewController {

    func openURL(url: NSURL) -> Bool {
        do {
            let application = try self.sharedApplication()
            return application.performSelector("openURL:", withObject: url) != nil
        }
        catch {
            return false
        }
    }

    func sharedApplication() throws -> UIApplication {
        var responder: UIResponder? = self
        while responder != nil {
            if let application = responder as? UIApplication {
                return application
            }

            responder = responder?.nextResponder()
        }

        throw NSError(domain: "UIInputViewController+sharedApplication.swift", code: 1, userInfo: nil)
    }

}

Xamarin.iOS version of @coyer answer: @coyer 的 Xamarin.iOS版本回答:

using System;
using Foundation;
using UIKit;
using MobileCoreServices;
using CoreFoundation;
using System.Linq;
using Newtonsoft.Json;
using System.Collections.Generic;
using ObjCRuntime;
using System.Runtime.InteropServices;

namespace Your.ShareExtension
{
public partial class ShareViewController : UIViewController
{
    public ShareViewController(IntPtr handle) : base(handle)
    {
    }

    string docPath = "";

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        try
        {
            var containerURL = new NSFileManager().GetContainerUrl("group.com.qsiga.startbss");
            docPath = $"{containerURL.Path}/share";

            //  Create directory if not exists
            try
            {
                NSFileManager.DefaultManager.CreateDirectory(docPath, true, null);
            }
            catch (Exception e)
            { }

            //  removing previous stored files
            NSError contentError;
            var files = NSFileManager.DefaultManager.GetDirectoryContent(docPath, out contentError);
            foreach (var file in files)
            {
                try
                {
                    NSError err;
                    NSFileManager.DefaultManager.Remove($"{docPath}/{file}", out err);
                }
                catch (Exception e)
                { }
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("ShareViewController exception: " + e);
        }
    }

    public override void ViewDidAppear(bool animated)
    {
        var alertView = UIAlertController.Create("Export", " ", UIAlertControllerStyle.Alert);

        PresentViewController(alertView, true, () =>
        {

            var group = new DispatchGroup();

            foreach (var item in ExtensionContext.InputItems)
            {

                var inputItem = item as NSExtensionItem;

                foreach (var provider in inputItem.Attachments)
                {

                    var itemProvider = provider as NSItemProvider;
                    group.Enter();
                    itemProvider.LoadItem(UTType.Data.ToString(), null, (data, error) =>
                                {
                                    if (error == null)
                                    {
                                        //  Note: "data" may be another type (e.g. Data or UIImage). Casting to URL may fail. Better use switch-statement for other types.
                                        //  "screenshot-tool" from iOS11 will give you an UIImage here
                                        var url = data as NSUrl;
                                        var path = $"{docPath}/{(url.PathComponents.LastOrDefault() ?? "")}";

                                        NSError err;
                                        NSFileManager.DefaultManager.Copy(url, NSUrl.CreateFileUrl(path, null), out err);
                                    }
                                    group.Leave();
                                });
                }
            }

            group.Notify(DispatchQueue.MainQueue, () =>
            {
                try
                {
                    var jsonData = JsonConvert.SerializeObject(new Dictionary<string, string>() { { "action", "incoming-files" } });
                    var jsonString = NSString.FromData(jsonData, NSStringEncoding.UTF8).CreateStringByAddingPercentEncoding(NSUrlUtilities_NSCharacterSet.UrlQueryAllowedCharacterSet);
                    var result = openURL(new NSUrl($"startbss://share?{jsonString}"));
                }
                catch (Exception e)
                {
                    alertView.Message = $"Error: {e.Message}";
                }
                DismissViewController(false, () =>
                {
                    ExtensionContext?.CompleteRequest(new NSExtensionItem[] { }, null);
                });
            });
        });
    }

    public bool openURL(NSUrl url)
    {
        UIResponder responder = this;
        while (responder != null)
        {
            var application = responder as UIApplication;
            if (application != null)
                return CallSelector(application, url);

            responder = responder?.NextResponder;
        }
        return false;
    }

    [DllImport(Constants.ObjectiveCLibrary, EntryPoint = "objc_msgSend")]
    static extern bool _callSelector(
        IntPtr target,
        IntPtr selector,
        IntPtr url,
        IntPtr options,
        IntPtr completionHandler
    );

    private bool CallSelector(UIApplication application, NSUrl url)
    {
        Selector selector = new Selector("openURL:options:completionHandler:");

        return _callSelector(
            application.Handle,
            selector.Handle,
            url.Handle,
            IntPtr.Zero,
            IntPtr.Zero
        );
    }
}
}

I'm able to get this working by accessing the shared UIApplication instance via key-value coding and calling openURL on that:我可以通过键值编码访问共享的UIApplication实例并在其上调用openURL来实现这一点:

let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as! UIApplication

let selector = NSSelectorFromString("openURL:")

let url = URL(string: "jptest://")!

application.perform(selector, with: url)

I was having this problem, and in iOS 11+ none of the previous answers work.我遇到了这个问题,在 iOS 11+ 中,以前的答案都不起作用。 I ended up adding a completion handler to my JavaScript code, and from there setting window.location="myapp://" .我最终在我的 JavaScript 代码中添加了一个完成处理程序,并从那里设置window.location="myapp://" It's a bit hacky but it doesn't look to bad and the user can follow along.它有点老套,但看起来还不错,用户可以跟上。

Not only there is no way (and won't be) to do this: there is no NEED to handle this in the app.不仅没有办法(也不会)这样做:没有必要在应用程序中处理这个问题。 The extension is supposed to handle this with the very same codebase as the main app.该扩展程序应该使用与主应用程序完全相同的代码库来处理这个问题。 You should create a framework with extension safe API shared between the app and the extesnion targets.您应该创建一个在应用程序和扩展目标之间共享扩展安全 API 的框架。

This is the top topic here: https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1这是这里的热门话题: https : //developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1

EDIT: This solution works for today extension (Widget).编辑:此解决方案适用于今天的扩展(小部件)。

An extension can open the hosting app:扩展程序可以打开托管应用程序:

- (IBAction)launchHostingApp:(id)sender
{
 NSURL *pjURL = [NSURL URLWithString:@"hostingapp://home"];
[self.extensionContext openURL:pjURL completionHandler:nil];
}

And like Apple says in Handling Commons Scenarios :就像苹果在处理公共场景中所说的那样:

An extension doesn't directly tell its containing app to open;扩展程序不会直接告诉其包含的应用程序打开; instead, it uses the openURL:completionHandler: method of NSExtensionContext to tell the system to open its containing app.相反,它使用 NSExtensionContext 的 openURL:completionHandler: 方法告诉系统打开其包含的应用程序。 When an extension uses this method to open a URL, the system validates the request before fulfilling it.当扩展程序使用此方法打开 URL 时,系统会在完成请求之前对其进行验证。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如果我打开包含共享扩展名中的应用程序,苹果会拒绝吗? - Will Apple Reject If I open containing app from share extension? iOS 操作扩展无法打开包含应用程序 - iOS action extension cannot open containing app 如何使用NSUserDefaults与包含应用程序共享数据? - 今日Extension Widget - How to share data with containing app using NSUserDefaults ? - Today Extension Widget 使用钥匙串在 iOS 扩展程序及其包含的应用程序之间共享? - Share between an iOS extension and its containing app with the keychain? 今日应用扩展小部件点击以打开包含应用 - Today App Extension Widget Tap To Open Containing App 从iOS Safari共享扩展打开主应用 - Open the main app from a iOS Safari Share Extension 打开包含今日扩展的应用程序时崩溃 - Crash when open containing app from today extension 共享扩展名.appex文件未与.app文件一起安装。 仅安装包含的应用程序 - share extension .appex file not installing with .app file. Only get to install the containing app iOS 8自定义键盘扩展程序不允许与包含的应用程序共享数据吗? - iOS 8 Custom Keyboard extension don't allow share data with containing app? 点击扩展程序(今天的扩展程序)后是否可以打开特定的viewcontroller(包含应用程序)? - is it possible to open specific viewcontroller(containing app) after tap on extension(today extension)?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM