openURL not work in Action Extension

IosIos8Ios App-Extension

Ios Problem Overview


I add following code:

- (IBAction)done {
    // Return any edited content to the host app.
    // This template doesn't do anything, so we just echo the passed in items.

    NSURL *url = [NSURL URLWithString:@"lister://today"];
    [self.extensionContext openURL:url completionHandler:^(BOOL success) {
        NSLog(@"fun=%s after completion. success=%d", __func__, success);
    }];
    [self.extensionContext completeRequestReturningItems:self.extensionContext.inputItems completionHandler:nil];

}

after I create the Action Extension target. But it can not work.

My purpose is that: when user view a photo in Photos.app (the iOS's default Photos.app or called gallery), and he click the share button to launch our extension view. We can transfer the image from Photos.app to my own app and deal or upload the image in my app.

I also try "CFBundleDocumentTypes" but it also can not work.

Any help will be appreciated.

Ios Solutions


Solution 1 - Ios

This is what I used to do:

UIWebView * webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
NSString *urlString = @"https://itunes.apple.com/us/app/watuu/id304697459";
NSString * content = [NSString stringWithFormat : @"<head><meta http-equiv='refresh' content='0; URL=%@'></head>", urlString];
[webView loadHTMLString:content baseURL:nil];
[self.view addSubview:webView];
[webView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:2.0];

Please note that in this case I am instantiating this call from the UIInputViewController.

This method should also work using the URL scheme from the containing app

UPDATE 04/17/2015: This does not work with iOS 8.3. We are looking for a solution and we will update the answer soon

UPDATE 06/01/2015: We found a solution that works in iOS 8.3

var responder = self as UIResponder?
    
while (responder != nil){
    if responder!.respondsToSelector(Selector("openURL:")) == true{
        responder!.callSelector(Selector("openURL:"), object: url, delay: 0)
    }
    responder = responder!.nextResponder()
}

This will find a suitable responder to send the openURL to.

You need to add this extension that replaces the performSelector for swift and helps in the construction of the mechanism:

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

UPDATE 06/15/2015: Objective-C

Someone asked for the code in Objective-C so here it is. I am not going to run it as I don't have the time right now but it should be quite straightforward:

UIResponder *responder = self;
while(responder){
    if ([responder respondsToSelector: @selector(OpenURL:)]){
        [responder performSelector: @selector(OpenURL:) withObject: [NSURL URLWithString:@"www.google.com" ]];
    }
    responder = [responder nextResponder];
}

As mentioned, I have not run this Objective-C code, it is just a conversion from the Swift code. Please let me know if you encounter any issues and the solution and I will update it. Nowadays, I am just using swift and unfortunately my brain is deprecating Objective-C

UPDATE 05/02/2016: Deprecated functions

As pointed by @KyleKIM the Selector functions have been replaced in Swift 2.2 by #selector. Also, there is a function that is deprecated and will probably get removed in Swift 3.0 so I am doing some research to find an alternative.

UPDATE 09/16/2016: XCode 8, Swift 3.0 and iOS10 The following code is still working on the mentioned versions. You will get some warnings:

let url = NSURL(string:urlString)
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
}

UPDATE 6/15/2017: XCode 8.3.3

let url = NSURL(string: urlString)
let selectorOpenURL = sel_registerName("openURL:")
let context = NSExtensionContext()
context.open(url! as URL, completionHandler: nil)
    
var responder = self as UIResponder?
    
while (responder != nil){
    if responder?.responds(to: selectorOpenURL) == true{
        responder?.perform(selectorOpenURL, with: url)
    }
    responder = responder!.next
}

Solution 2 - Ios

Try this code.

    UIResponder* responder = self;
    while ((responder = [responder nextResponder]) != nil)
    {
        NSLog(@"responder = %@", responder);
        if([responder respondsToSelector:@selector(openURL:)] == YES)
        {
            [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:urlString]];
        }
    }

Solution 3 - Ios

This is by design. We don't want Custom Actions to become app launchers.

Solution 4 - Ios

Apple accepted the following solution, which is the "same" code that a host app would use. It works on all iOS 8 versions to date (tested on iOS 8.0 - iOS 8.3).

NSURL *destinationURL = [NSURL URLWithString:@"myapp://"];

// Get "UIApplication" class name through ASCII Character codes.
NSString *className = [[NSString alloc] initWithData:[NSData dataWithBytes:(unsigned char []){0x55, 0x49, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E} length:13] encoding:NSASCIIStringEncoding];
if (NSClassFromString(className)) {
    id object = [NSClassFromString(className) performSelector:@selector(sharedApplication)];
    [object performSelector:@selector(openURL:) withObject:destinationURL];
}

Solution 5 - Ios

Worked solution in Swift 3.0 & 4.0:

// For skip compile error. 
func openURL(_ url: URL) {
    return
}

func openContainerApp() {
    var responder: UIResponder? = self as UIResponder
    let selector = #selector(openURL(_:))
    while responder != nil {
        if responder!.responds(to: selector) && responder != self {
            responder!.perform(selector, with: URL(string: "containerapp://")!)
            return
        }
        responder = responder?.next
    }
}

Explanation:

In extension, api is limited by compiler to not let you use openURl(:URL) like in container app. However the api is still here.

And we can't perform method in our class until we declare it, what we really want is let UIApplication to perform this method.

Recall to responder chain, we can use

    var responder: UIResponder? = self as UIResponder
    responder = responder?.next

to loop to UIApplication object.

And my apps with this method pass the review process, so don't worry to use it.

Solution 6 - Ios

Following code works on Xcode 8.3.3, iOS10, Swift3 and Xcode 9, iOS11, Swift4 without any compiler warnings:

func openUrl(url: URL?) {
    let selector = sel_registerName("openURL:")
    var responder = self as UIResponder?
    while let r = responder, !r.responds(to: selector) {
        responder = r.next
    }
    _ = responder?.perform(selector, with: url)
}

func canOpenUrl(url: URL?) -> Bool {
    let selector = sel_registerName("canOpenURL:")
    var responder = self as UIResponder?
    while let r = responder, !r.responds(to: selector) {
        responder = r.next
    }
    return (responder!.perform(selector, with: url) != nil)
}

Make sure your app supports Universal Links, otherwise it will open the link in browser. More info here: https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html

Solution 7 - Ios

Working solution (tested on iOS 9.2) for Keyboard Extension. This category adds special method for access to hidden sharedApplication object and then call openURL: on it. (Of course then you have to use openURL: method with your app scheme.)

extension UIInputViewController {

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

}

Solution 8 - Ios

It seems to be a bug, because docs say:

> Opening the Containing App > > In some cases, it can make sense for an extension to request its > containing app to open. For example, the Calendar widget in OS X opens > Calendar when users click an event. To ensure that your containing app > opens in a way that makes sense in the context of the user’s current > task, you need to define a custom URL scheme that both the app and its > extensions can use. > > 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.

I reported it today: http://openradar.appspot.com/17376354 You should dupe it, if you have some free time.

Solution 9 - Ios

NSExtensionContext only support openURL function in today extension ,this is described in apple's documents about NSExtensionContext.The original words is "Each extension point determines whether to support this method, or under which conditions to support this method. In iOS 8.0, only the Today extension point supports this method."

Solution 10 - Ios

A possible workaround: Create and add a small UIWebView to your view and run it's method loadRequest with the url scheme you set above. This is a workaround and I'm not sure what Apple will say about it. Good luck!

Solution 11 - Ios

An updated version of Julio Bailon's answer with modern Swift syntax:

let url = NSURL(string: "scheme://")!
var responder: UIResponder? = self
while let r = responder {
    if r.respondsToSelector("openURL:") {
        r.performSelector("openURL:", withObject: url)
        break
    }
    responder = r.nextResponder()
}

There is no need for an extension for NSObject now.

Note: you must wait for the view to be attached to the view hierarchy before calling this code otherwise the responder chain can't be used.

Solution 12 - Ios

Solution for the latest iOS SDK 10.2. All previous solutions use deprecated api. This solution is based on searching UIApplication UIResponder of the hosting application (This app which create execution context for our extension). The solution can only be provided in Objective-C because there is a 3 arguments method to invoke and this is impossible to do with performSelector: methods. To invoke this not deprecated method openURL:options:completionHandler: we need to use NSInvocation instance which is unavailable in Swift. The provided solution can be invoked from Objective-C and Swift (any version). I need to say that I don't know yet if provided solution will be valid for apple review process.

UIViewController+OpenURL.h

#import <UIKit/UIKit.h>
@interface UIViewController (OpenURL)
- (void)openURL:(nonnull NSURL *)url;
@end

UIViewController+OpenURL.m

#import "UIViewController+OpenURL.h"

@implementation UIViewController (OpenURL)

- (void)openURL:(nonnull NSURL *)url {
    
    SEL selector = NSSelectorFromString(@"openURL:options:completionHandler:");
    
    UIResponder* responder = self;
    while ((responder = [responder nextResponder]) != nil) {
        NSLog(@"responder = %@", responder);
        if([responder respondsToSelector:selector] == true) {
            NSMethodSignature *methodSignature = [responder methodSignatureForSelector:selector];
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
            
            // Arguments
            NSDictionary<NSString *, id> *options = [NSDictionary dictionary];
            void (^completion)(BOOL success) = ^void(BOOL success) {
                NSLog(@"Completions block: %i", success);
            };
            
            [invocation setTarget: responder];
            [invocation setSelector: selector];
            [invocation setArgument: &url atIndex: 2];
            [invocation setArgument: &options atIndex:3];
            [invocation setArgument: &completion atIndex: 4];
            [invocation invoke];
            break;
        }
    }
}
    
@end

From Swift 3 You can execute this only if Your view controller is in view hierarchy. This is the code how I'm using it:

override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let context = self.extensionContext!
        let userAuthenticated = self.isUserAuthenticated()

        if !userAuthenticated {
            let alert = UIAlertController(title: "Error", message: "User not logged in", preferredStyle: .alert)
            let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in
                context.completeRequest(returningItems: nil, completionHandler: nil)
            }
            let login = UIAlertAction(title: "Log In", style: .default, handler: { _ in
                //self.openContainingAppForAuthorization()
                let url = URL(string: "fashionapp://login")!
                self.open(url)
                context.completeRequest(returningItems: nil, completionHandler: nil)
            })
            
            alert.addAction(cancel)
            alert.addAction(login)
            present(alert, animated: true, completion: nil)
        }
    }

Solution 13 - Ios

A bit safer option using recursion

func handleUrl(_ hostUrl: URL?) {
        fileUrl.map { URL(string: yourUrlSchemeId + "://" + $0.absoluteString) }.map { finalUrl in
            let selector = #selector(openURL(_:))
            func proccessNext(_ responder: UIResponder?) {
                responder.map {
                    if $0.responds(to: selector) {
                        $0.perform(selector, with: finalUrl)
                    } else {
                        proccessNext($0.next)
                    }
                }
            }
            proccessNext(self.next)
        }
    }
    @objc func openURL(_ url: URL) {
        return
    }


Solution 14 - Ios

Not every app extension type supports "extensionContext openURL".

I tested on iOS 8 beta 4 and found Today extension supports it, but keyboard extension does not.

Solution 15 - Ios

Only the Today Extension seems to work.
It's not 100% documented, but an apple employee specifically says that Keyboard extensions do not support openURL:completionHandler.
The documentation says:

> Each extension point determines whether to support this method, or under which conditions to support this method.

So in practice, Share, Action, Keyboard, and Document provider do not work for anyone (beta 5) and only Today Extension supports it.

Solution 16 - Ios

As apple document " A Today widget (and no other app extension type) can ask the system to open its containing app by calling the openURL:completionHandler: method of the NSExtensionContext class."

For other Extension, I used this solution

UIWebView * webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
NSString *urlString = @"ownApp://"; // Use other application url schema.

NSString * content = [NSString stringWithFormat : @"<head><meta http-equiv='refresh' content='0; URL=%@'></head>", urlString];
[webView loadHTMLString:content baseURL:nil];
[self.view addSubview:webView];
[webView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:1.0];

Solution 17 - Ios

My guess is that this is intentionally not possible. The openURL:completionHandler: block says that it may not be supported in all extension types, and the action extension docs explicitly say:

> If you want to help users share content on a social website or give users updates on information they care about, the Action extension point is not the right choice.

I think a share extension might be more appropriate, but the docs for both types suggest that the experience should be embedded in the host app, not taking the users to your app, so it might not allow that for that either. So, perhaps follow the share extension docs and just upload your image from within the extension UI as it suggests?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLaurence FanView Question on Stackoverflow
Solution 1 - IosJulio BailonView Answer on Stackoverflow
Solution 2 - IosDongHyun JangView Answer on Stackoverflow
Solution 3 - IosIan BairdView Answer on Stackoverflow
Solution 4 - IosHank BrekkeView Answer on Stackoverflow
Solution 5 - IosAlen LiangView Answer on Stackoverflow
Solution 6 - IosDeniss FedotovsView Answer on Stackoverflow
Solution 7 - IosValentin SherginView Answer on Stackoverflow
Solution 8 - IosArek HolkoView Answer on Stackoverflow
Solution 9 - Iosfiona zhouView Answer on Stackoverflow
Solution 10 - IosnurnachmanView Answer on Stackoverflow
Solution 11 - IosPulsarView Answer on Stackoverflow
Solution 12 - IosMarcin KapustaView Answer on Stackoverflow
Solution 13 - IosArtem ZinkovView Answer on Stackoverflow
Solution 14 - IosVince YuanView Answer on Stackoverflow
Solution 15 - Iosn8trView Answer on Stackoverflow
Solution 16 - IosAnand MishraView Answer on Stackoverflow
Solution 17 - IosJesse RusakView Answer on Stackoverflow