大手のキーボードアプリは当たり前に実装している「収容アプリを開く」機能ですが、調べるとなかなか情報が出てきません。
普通のアプリ開発ではUIApplication.shared.open
を使うのではないかと思うのですが、App ExtensionではそもそもUIApplication.shared
が取れないのでopen
出来ないんです。
Stack OverflowでiOS14でも動作するコードを見つけたので共有します。
以下コードをUIInputViewController
などに書き、引数のURLに収容アプリのURL Schemeを入れてあげればOKです。URL Schemeの作り方はこちらが参考になりました。
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)
}
ただ、何をしているのか全くわからなかったので調べてみたことを付記します。
何をしているのか
ドキュメントによると
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の仕組み」という記事で詳しく説明されていました。
条件部分のresponds
という関数はドキュメントによると「Responderがセレクタに対応するメソッドを使えるか」のような情報を持っているらしいので、この部分では「openURL
できるResponderを探している」ということになりそうです。実験してみたところ、このResponder
はUIApplication
でした。
最後の1行で取得したresponder
を用いてセレクタを実行します。第二引数はopenURL
の引数のurl
です。返り値はUnmanaged<AnyObject>!
という怖そうな型なのですが、使わないので破棄されています。
以上をまとめると、openURL
に対応するセレクタを作り、それを使えるUIResponder
を探し、openURL
を実行する、という手順のようでした。
なぜそうしているのか
まずsel_registerName
は必ずしも用いなくて大丈夫です。現在の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
が取れるならわざわざセレクタを経由する必要はなさそうですよね。しかしそうではありません。
@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
自体が取得できませんでした。
let selector = #selector(open(_:options:completionHandler:))
今後使えなくなってしまうとかなり問題なので、何かご存知の方がいらっしゃったら教えていただけると嬉しいです。