[Swift] Keyboard Extensionから収容アプリを開く

大手のキーボードアプリは当たり前に実装している「収容アプリを開く」機能ですが、調べるとなかなか情報が出てきません。

普通のアプリ開発では`UIApplication.shared.open`を使うのではないかと思うのですが、App Extensionではそもそも`UIApplication.shared`が取れないので`open`出来ないんです。

[Stack Overflow](https://stackoverflow.com/questions/40019521/open-my-application-from-my-keyboard-extension-in-swift-3-0)でiOS14でも動作するコードを見つけたので共有します。

以下コードを`UIInputViewController`などに書き、引数のURLに収容アプリのURL Schemeを入れてあげればOKです。URL Schemeの作り方は[こちら](https://qiita.com/yamataku29/items/67f12142522306c3f86a)が参考になりました。

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

ただ、何をしているのか全くわからなかったので調べてみたことを付記します。

# 何をしているのか

[ドキュメント](https://developer.apple.com/documentation/objectivec/1418557-sel_registername)によると

>Registers a method with the Objective-C runtime system, maps the method name to a selector, and returns the selector value.

とのことです。Objective-Cは書けないので意味がわからなかったのですが、どうやら「`OpenURL`というメソッド名に対応するメソッドの内部表現」を予約して、その内部表現を表すセレクタを返す関数のようです。

次に行われているのが`UIResponder`を次々に代入していく操作です。`UIInputViewController`は`UIResponder`を継承しているのでキャストは成功します。この部分は「Responder Chain」と呼ばれているらしく、「[【Swift】Responder Chainの仕組み](https://qiita.com/shiz/items/5a75fbd903de4a474b1d)」という記事で詳しく説明されていました。

条件部分の`responds`という関数は[ドキュメント](https://developer.apple.com/documentation/objectivec/nsobjectprotocol/1418583-responds)によると「Responderがセレクタに対応するメソッドを使えるか」のような情報を持っているらしいので、この部分では「`openURL`できるResponderを探している」ということになりそうです。実験してみたところ、この`Responder`は`UIApplication`でした。

最後の1行で取得した`responder`を用いてセレクタを実行します。第二引数は`openURL`の引数の`url`です。返り値は`Unmanaged<AnyObject>!`という怖そうな型なのですが、使わないので破棄されています。

以上をまとめると、`openURL`に対応するセレクタを作り、それを使える`UIResponder`を探し、`openURL`を実行する、という手順のようでした。

# なぜそうしているのか
まず`sel_registerName`は必ずしも用いなくて大丈夫です。現在のSwiftではもう少し現代的なセレクタの記法が使えるので次のように書くことができます。

```Swift
    @objc func openURL(_ url: URL) {}  //#selector(openURL(_:))はこの関数がないと作れない

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

このほうが若干現代的になった気がしますね。

ところで最終的に取得できる`responder`は結局`UIApplication`でした。結局`UIApplication`が取れるならわざわざセレクタを経由する必要はなさそうですよね。しかしそうではありません。

```Swift
    @objc func openURL(_ url: URL) {}  //#selector(openURL(_:))はこの関数がないと作れない

    func openUrl(url: URL?) {
        let selector = #selector(openURL(_:))
        var responder = self as UIResponder?
        while let r = responder, !r.responds(to: selector) {
            responder = r.next
        }
        if let uiApplication = responder as? UIApplication{
            uiApplication.open
        }
    }
```

試しにこう書いてみると、エラーが出ます。

>'open(_:options:completionHandler:)' is unavailable in application extensions for iOS

とのこと。なるほど、これを回避するためにわざわざセレクタを使って迂回するんですね。

何にしても、まどろっこしい書き方の動機は「`UIApplication`を`UIResponder`として扱ったまま、`openURL`を呼びたい！」という部分にありそうです。

# 気になった点
めちゃくちゃ黒魔術な気がします。こういうものでしょうか。

`openURL`が`deprecated`なのが気になります。iOS10から既に`deprecated`なので、そろそろ廃止されてもおかしくないはずです。そうなると`open`の場合は`open:options:completionHandler:`となってしまうため、引数が3つになります。残念ながら`UIResponder`には3つ以上の引数を取る場合の`perform`が無いため、詰みます。

ただ理解が甘いためか、下のようにセレクタを書いても`responder`自体が取得できませんでした。

```Swift
let selector = #selector(open(_:options:completionHandler:))
```

今後使えなくなってしまうとかなり問題なので、何かご存知の方がいらっしゃったら教えていただけると嬉しいです。