我想为我的应用创建一个 Android 样式共享功能。我创建了一个共享扩展,当您在 Stock Photo 应用程序中选择图片并按下共享时会调用该扩展。现在我希望将这些图片发送到主应用程序并在那里进行处理。我现在的问题是:
- 在共享扩展窗口上按下按钮后,iOS 可以打开我的应用程序吗?
- 如何在我的主应用程序中获取图片文件?
我想为我的应用创建一个 Android 样式共享功能。我创建了一个共享扩展,当您在 Stock Photo 应用程序中选择图片并按下共享时会调用该扩展。现在我希望将这些图片发送到主应用程序并在那里进行处理。我现在的问题是:
Swift 4+(在 iOS 13 上测试)
@objc
应添加到 的声明中openURL
,即
@objc func openURL(_ url: URL) -> Bool {
// Code below.
}
没有它,你会看到这个编译器错误:
Argument of '#selector' refers to instance method 'openURL' that is not exposed to Objective-C
Swift 3.1 中的工作解决方案(在 iOS10 中测试):
您需要创建自己的 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
}
编辑:澄清说明:
openURL
是 UIApplication 的一种方法 - 因为您的 ShareExtension 不是从 UIApplication 派生的,所以我添加了自己的openURL
与 UIApplication 相同的定义,以保持编译器满意(以便#selector(openURL(_:)可以被发现)。
然后我遍历响应者,直到找到真正源自UIApplication
并调用它的响应者openURL
。
更精简的示例代码,它将 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
}
}
从技术上讲,您无法从共享扩展中打开包含应用程序,但您可以安排本地通知,这就是我最终要做的。就在我调用 super.didSelectPost 之前,我用一些文本安排本地通知,如果用户想要打开包含的应用程序,他们可以,如果不是 - 他们可以继续他们的工作流程。我什至认为它比自动打开包含应用程序并破坏他们正在做的事情更好。
目前没有办法做到这一点。共享扩展无法打开包含的应用程序。
共享扩展的预期方法是它们自己处理所有必要的工作。扩展程序可以通过使用自定义框架与其包含的应用程序共享代码,因此在大多数情况下这没有问题。
如果您想让数据对您的应用程序可用,您可以设置一个应用程序组,以便您拥有一个共享目录。扩展程序可以在那里写入数据,应用程序可以读取它。不过,直到用户下次启动应用程序时才会发生这种情况。
我用一个技巧从共享扩展打开了主机应用程序。使用具有清晰背景颜色的 web 视图。下面是代码
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];
在主机应用程序中实现自定义 url 架构并调用 openURL(url:) 方法
像 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版本的@coyer 答案:
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
);
}
}
}
我可以通过键值编码访问共享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)
我遇到了这个问题,在 iOS 11+ 中,以前的答案都不起作用。我最终在我的 JavaScript 代码中添加了一个完成处理程序,并从那里设置window.location="myapp://"
. 这有点hacky,但看起来还不错,用户可以跟随。
不仅没有办法(也不会)这样做:无需在应用程序中处理此问题。该扩展应该使用与主应用程序相同的代码库来处理这个问题。您应该创建一个在应用程序和扩展目标之间共享扩展安全 API 的框架。
额外的理由:在扩展中,您必须使用更小的内存余量,这意味着:如果您在主应用程序中使用大小合适的图像,您可能会崩溃和烧毁。在扩展中,您必须使用 jpeg 或合理的小尺寸,即使这样也要确保尺寸足够小,否则您将被引导出尝试将图像从磁盘解压到内存中(请参阅上面的大小限制)
编辑:此解决方案适用于今天的扩展(小部件)。
扩展程序可以打开托管应用程序:
- (IBAction)launchHostingApp:(id)sender
{
NSURL *pjURL = [NSURL URLWithString:@"hostingapp://home"];
[self.extensionContext openURL:pjURL completionHandler:nil];
}
就像苹果在处理公共场景中所说的那样 :
扩展程序不会直接告诉其包含的应用程序打开;相反,它使用 NSExtensionContext 的 openURL:completionHandler: 方法告诉系统打开其包含的应用程序。当扩展程序使用此方法打开 URL 时,系统会在执行请求之前验证请求。