42

I want to create an Android Style share feature for my app. 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?
  2. How do I get the picture files inside my main app?
jscs
  • 63,694
  • 13
  • 151
  • 195
zanzoken
  • 787
  • 4
  • 12
  • 18

10 Answers10

63

Swift 4+ (tested on iOS 13)

@objc should be added to the declaration of openURL, that is,

@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):

You need to create your own URL Scheme, then add this function to your ViewController and call it with 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).

Then I go through the responders until I find one that is really derived from UIApplication and call openURL on that.

More stripped-down-example-code which copies files in a ShareExtension to a local directory, serializing filenames and calling openURL on another app:

//
//  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
}
}
Diego Jiménez
  • 1,398
  • 1
  • 15
  • 26
coyer
  • 4,122
  • 3
  • 28
  • 35
  • Its objective-C version please.? – Md Rais Jul 19 '17 at 11:26
  • 4
    @coyer : How to open it from share extension can you explain more – Abhishek Thapliyal Aug 26 '17 at 06:11
  • Hi can you explain further. When I put the call for *openURL* in configurationItems of shareViewController, it crashes. – Ace Rivera Oct 03 '17 at 16:35
  • 1
    @Tom Harrington: Yes it was accepted without any problems. – coyer Mar 02 '18 at 09:47
  • This doesn't work in iOS 11 exactly. Problem is `openURL` is gone in favor of `open`, which has 3 params instead of 1. There's no way to use `perform` with 3 or more arguments, only 1 or 2 (I'm not kidding). So here's the alternative I'll try later: https://stackoverflow.com/questions/40533914/is-there-any-alternative-for-nsinvocation-in-swift – sudo Nov 06 '18 at 06:01
  • 2
    I'm also very surprised they accept this since you're accessing a private API, which Apple explicitly has a rule against. – sudo Nov 06 '18 at 06:10
  • Worked perfectly on iOS Deployment Target 12.0, and running on iOS 13 – mourodrigo Jan 30 '20 at 18:16
  • 1
    openURL not calling in iOS 13.1 and not opening main/host app. Previous version works fine. Any findings ? – Jamshed Alam Feb 21 '20 at 19:00
  • @All, Is this the reason for not working the UIApplication, UIResponder , sharedapplication not opening the host application from share extension app ? Anyone here ? https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html#//apple_ref/doc/uid/TP40014214-CH2-SW6 – Jamshed Alam Feb 22 '20 at 07:19
  • I just added a Xamarin.iOS version of this answer :) – David Riha Oct 19 '20 at 16:41
  • It definitely the best answer to this question, it works just fine for my app. But there is another problem, when my containing app is launched (not wake up from background) my my extension (actually its an Action Extension), my rootViewController is an EMPTY UIViewcontroller(not it's subclass), and a blank view appeared in my app. – Roen Dec 04 '20 at 01:35
10

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. I even think its a better approach than automatically opening containing app and disrupting what they are doing.

Cherpak Evgeny
  • 2,659
  • 22
  • 29
7

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.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Can a action extension open the app? – zanzoken Dec 17 '14 at 09:24
  • 1
    No. The only way an extension can open an app is if the app declares a custom URL scheme, and the extension uses `[NSExtensionContext openURL:error:]`. But on iOS, that method only works in today extensions, not in other extension types. – Tom Harrington Dec 17 '14 at 17:21
  • 30
    I downvoted this answer because both DropBox and Maps.me open their apps after using their share extension. So there is a way. Now we just need to find it.... – Tycho Pandelaar Jan 04 '15 at 10:52
  • 6
    Those apps **do not have share extensions**. Dropbox has a "today" extension and a file provider extension, and Maps.me doesn't have any extensions at all. – Tom Harrington Jan 04 '15 at 20:55
  • Still going to leave the down vote in place, though? – Tom Harrington Jan 06 '15 at 18:34
  • I upvoted it on the 4th of Jan: "You last voted on this answer Jan 4 at 10:50. Your vote is now locked in unless this answer is edited" – Tycho Pandelaar Mar 20 '15 at 15:33
  • 1
    Is this still meant to be status quo -- can anyone help me out with this? http://stackoverflow.com/questions/32228182/code-to-share-file-path-file-between-a-share-extension-and-ios-app – Vrashabh Irde Sep 04 '15 at 15:00
  • 8
    So what kind of extension is the "Copy to iBooks"? It appears in the share extension view whenever one tries to share a PDF. I've also seen other non Apple extensions with the same behavior. There has to be a way to make this work. – Bjørn Ruthberg Feb 12 '16 at 08:30
  • The "Opener - open links in apps" seems to have found a way to do this, so it's possible.. – Paulo Cesar Feb 15 '16 at 11:34
  • @PauloCesar what are you talking about? – Tom Harrington Feb 15 '16 at 16:23
  • 5
    @TomHarrington it's an app that does exactly what you guys are saying it's impossible to do: https://itunes.apple.com/br/app/opener-open-links-in-apps/id989565871 – Paulo Cesar Feb 15 '16 at 16:32
  • The Truecaller app can do this, and it is using an action extension. The extension is invoked from the Phone app's recent call history - share contact. The extension launches, and the user can then click on a button in the extension's GUI, when they do then the Truecaller container app launches. – Gruntcakes Aug 18 '16 at 00:39
  • More Promising way http://stackoverflow.com/questions/32228182/code-to-share-file-path-file-between-a-share-extension-and-ios-app – Ratul Sharker Dec 30 '16 at 07:22
  • @RatulSharker that technique will probably make Apple reject your app. – Tom Harrington Dec 30 '16 at 16:36
  • in the answer it was said that the app was accepted. thats why i commented. By the way this way is better than nothing you can do, in case you use in house ipa distribution. But the answer does not full fill necessary aspect of the question. – Ratul Sharker Dec 30 '16 at 17:26
  • This is a common pattern for redirecting to the host app when the user has not authenticated. Download Tumblr and share a URL from Safari before logging in. – user Apr 12 '17 at 05:45
  • @TomHarrington, My app share files from gmail inbox using share extension( with app group )through container app to host app. openURL not calling in iOS 13.1 and not opening main/host app from container app. i just click the container app and noting happen. Previous version works fine. Any findings ? – Jamshed Alam Feb 22 '20 at 02:31
  • Downvote: App Canva opens Canva as soon we select photo to share into Canva icon. So the answer is wrong and misleading. – Akash Kava Sep 29 '22 at 05:11
  • @AkashKava This answer is from eight years ago and the first word in it is “currently”. Do you expect every answer on this site to be re-edited every year forever? – Tom Harrington Sep 29 '22 at 15:46
4

Implement custom url schema in host app and call openURL(url:) method

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

}
byJeevan
  • 3,728
  • 3
  • 37
  • 60
SPatel
  • 4,768
  • 4
  • 32
  • 51
3

I opened the host app from shared extension with a trick. Using a webview with clear background color. 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];
Waqas
  • 959
  • 1
  • 8
  • 17
  • It worked for me. Have tried and verified. Can you share your code ? – Waqas Feb 15 '16 at 19:07
  • 1
    unfortunately it does not seem to work on iOS9 anymore :/ the webview delegate `- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType` will be called but neither `- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error` nor `- (void)webViewDidFinishLoad:(UIWebView *)webView` will, no error, just an ignored custom url :/ – Hofi Mar 29 '16 at 20:51
3

I'm able to get this working by accessing the shared UIApplication instance via key-value coding and calling openURL on that:

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

let selector = NSSelectorFromString("openURL:")

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

application.perform(selector, with: url)
Josh
  • 2,324
  • 1
  • 21
  • 29
  • Apple explicitly says extensions are not allowed to use UIApplication. It may work, but there's a high chance Apple will reject the app. – alekop Jan 19 '22 at 19:31
3

Xamarin.iOS version of @coyer answer:

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
        );
    }
}
}
David Riha
  • 1,368
  • 14
  • 29
  • what version of iOS are you testing with? I just tried this on iOS 14 and cannot get the main app to launch – user5021816 Oct 19 '20 at 21:44
  • Never mind. I was calling openURL from ViewDidLoad, not ViewDidAppear. Switched and it's working now. Thank you for sharing! – user5021816 Oct 19 '20 at 21:53
  • Obj-c implementation: ```-(BOOL) openEventer: (NSURL *) url { UIResponder * responder = self; while(responder != nil) { if([responder isKindOfClass:[UIApplication class]]) { UIApplication * application = (UIApplication *) responder; if (application != nil) { SEL s = NSSelectorFromString(@"openURL:"); return [application performSelector:s withObject:url]; } } responder = [responder nextResponder]; } return false; }``` – Heitara Dec 07 '20 at 00:01
0

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.

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

Extra rationale: in the extension you'd have to work with a much smaller memory allowance meaning: if you use the images of decent size as in the main app you will likely crash and burn. In extension you'd have to work with jpeg or reasonable small size and even then make sure size is small enough otherwise you'd be booted out trying to unpack the image from disk into memory (see size limitation above)

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66
  • Even using a common code base, extensions have a significantly lower memory allowance. I understand Apple wants everything to happen without leaving the app, however there are use cases where more memory is needed. – Barnyard Jan 17 '20 at 23:26
  • Batch jobs to the rescue. Extension spools them. Host application processes memory hungry jobs. Seriously, you comment has some merit. I just never run into the memory limitations (however stringent) for the extensions (that are meant to be simple and not burden the host app with lots of heap consumed) – Anton Tropashko Jan 20 '20 at 10:31
  • I see what you are saying. I'm sharing a pdf from my app via Telegram, – Anton Tropashko Jan 21 '20 at 08:00
  • spinner starts and never ever ends. They could've spared the users of a share extension (that does not work) altogether – Anton Tropashko Jan 21 '20 at 08:00
0

I was having this problem, and in iOS 11+ none of the previous answers work. I ended up adding a completion handler to my JavaScript code, and from there setting window.location="myapp://". It's a bit hacky but it doesn't look to bad and the user can follow along.

Nylon
  • 1
  • 1
-6

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. When an extension uses this method to open a URL, the system validates the request before fulfilling it.

Zubair
  • 5,833
  • 3
  • 27
  • 49