Home » Ios » launch containing app from iOS8 Custom Keyboard

launch containing app from iOS8 Custom Keyboard

Posted by: admin February 24, 2018 Leave a comment

Questions:

I want to launch my containing app.

I tried using URL schemes.

The URL scheme launched the app from other places – so the problem is not there.

Looks like this object is nil:

   self.extensionContext

thus i can’t run this method:

[self.extensionContext openURL:url completionHandler:nil];

Can I launch my app? Do URL Schemes work in a custom keyboard?

thanks!

Answers:

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]];
        }
    }

Questions:
Answers:

To answer MY OWN question:

In an iOS8 custom keyboard, the extensionContext object is nil, thus I can’t use it to launch the containing app.

The workaround I came up with is:

  1. create a url scheme for your app
  2. add a UIWebView to your inputView
  3. load the url scheme for your containing app in the webview

I’m not sure if Apple will allow this to happen, but it works now.

Questions:
Answers:

Here is 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.)

// Usage:
// UIInputViewController.openURL(NSURL(string: "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)
    }

}

Lately I developed slightly different approach:

// Usage:
// UIApplication.🚀sharedApplication().🚀openURL(NSURL(string: "your-app-scheme://")!)

extension UIApplication {

    public static func 🚀sharedApplication() -> UIApplication {
        guard UIApplication.respondsToSelector("sharedApplication") else {
            fatalError("UIApplication.sharedKeyboardApplication(): `UIApplication` does not respond to selector `sharedApplication`.")
        }

        guard let unmanagedSharedApplication = UIApplication.performSelector("sharedApplication") else {
            fatalError("UIApplication.sharedKeyboardApplication(): `UIApplication.sharedApplication()` returned `nil`.")
        }

        guard let sharedApplication = unmanagedSharedApplication.takeUnretainedValue() as? UIApplication else {
            fatalError("UIApplication.sharedKeyboardApplication(): `UIApplication.sharedApplication()` returned not `UIApplication` instance.")
        }

        return sharedApplication
    }

    public func 🚀openURL(url: NSURL) -> Bool {
        return self.performSelector("openURL:", withObject: url) != nil
    }

}

Questions:
Answers:

Apple does not allow any app extensions other than Today extensions to open the containing app.

From the guidelines:

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.

You can check Here

Questions:
Answers:

I tried to do above solutions for latest xcode 8.2 and swift 3.0.

Unfortunately. I can’t get it work. So I found my own solution and it works well in swift 3.0, xcode 8.2

   func openURL(_ url: URL) {
        return
   }

   func openApp(_ urlstring:String) {

       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: urlstring)!)
              return
             }
             responder = responder?.next
          }
   }

  // Usage
  //call the method like below
  //self.openApp(urlString)

  //URL string need to included custom scheme.
  //for example, if you created scheme name = customApp
  //urlString will be "customApp://?[name]=[value]"
  //self.openApp("customApp://?category=1")

Questions:
Answers:

According to Apple’s documentation https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/index.html, you should use NSExtensionContext like this:

NSExtensionContext *ctx = [[NSExtensionContext alloc] init];
[ctx openURL:[NSURL URLWithString:@"myapp://"] completionHandler:^(BOOL success){return ;}];

Questions:
Answers:

In an App Extension (ex: custom keyboard), that would have been handled through UIViewController.extensionContext but as of iOS 8.1.2, the field is nil in the following Swift call:

self.extensionContext?.openURL(appURL, completionHandler: nil)
// Does nothing because extensionContext is nil (iOS 8.1)

Actually, it is not possible either to use Application.sharedApplication.openURL(...) in an App Extension as stated in Apple documentation.

So, as of 8.1.2, the workaround is to use a dumb UIWebView to redirect to the containing app like this:

let webView = UIWebView(frame: CGRectMake(0, 0, 0, 0));
let url = "MyKeyboard://";
let content = "<head><meta http-equiv='refresh' content='0; URL=\(url)'></head>";
webView.loadHTMLString(content, baseURL: nil);
self.view.addSubview(webView);
let delayInSeconds = 2.0
let startTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * Double(NSEC_PER_SEC)))
dispatch_after(startTime, dispatch_get_main_queue()) { () -> Void in
    webView.removeFromSuperview()
}

Where MyKeyboard:// must be defined accordingly as URL scheme in the containing app.

Please take note that I don’t know yet whether this is approved by Apple. Forwarding to apps from custom keyboards may be bad enough for user experience.

Questions:
Answers:

This is what I found to open any URL using what has been described above:

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

NOTE: As of iOS 8.3 Apple has killed this method

Questions:
Answers:

A Swift 2.2 version, akin to DongHyun Jang’s answer for Objective-C:

func webLink(urlString: String) {
    let url = NSURL(string: urlString)
    var responder: UIResponder? = self
    while let r = responder {
        if r.respondsToSelector("openURL:") {
            r.performSelector("openURL:", withObject: url)
            break;
        }
        responder = r.nextResponder()
    }  
}

Questions:
Answers:

As per Apple review guidelines for Extension, it is now not advised to open external apps from the extension.

For reference please see apple review guidelines

Section 4.4.1

They must not:

  • Include marketing, advertising, or in-app purchases;
  • Launch other apps besides Settings; or
Questions:
Answers:

I struggled with this for a couple of days. I could not get the UIWebView working inside the custom keyboard.

Fix: put it inside the viewDidAppear:animated instead of viewDidLoad (on another note this is where the code for the keyboard’s custom height should stay as well).

Code:

- (void)viewDidAppear:(BOOL)animated {
    UIWebView * webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    NSString * content = @"<a href='your_app_schema://your_params'>anchor</a>";
    [webView loadHTMLString:content baseURL:nil];
    [self.view addSubview:webView];
}