iOSの共有機能で自作アプリを起動したいときはShare ExtensionではなくSiriKitショートカットを使うべきだった話


やりたいこと:他のアプリから自分のアプリを起動したい

iOSで自作のアプリに対して、他のアプリから文字列や画像などを受け取りつつ自作アプリを起動したいと思うことがあると思います。例えばSafariでWebサイトを見ている時に文字列や画像を選択して「共有」ボタンを押すと以下のような画面(共有シート)が立ち上がります。

ここで「メッセージ」や「Twitter」のアイコンを押して友達に送ったりすることがよくあると思います。

「メッセージ」や「Twitter」ではなく、自分のアプリが立ち上げられたら良いのに、と思ってこの仕組みを調べてみました。この仕組みはShare ExtensionというApp Extensionの一種です。確かに他のアプリから「共有」ボタン経由で自作のアプリの処理を行うことができるのですが、実はここで立ち上げられるのはあくまでもExtensionの画面になります。「メッセージ」にしても「Twitter」にしても共有ボタンから立ち上がるのは投稿するだけの専用画面で、「メッセージ」や「Twitter」のアプリ本体が起動するわけではありません。

なので、画面1枚を新たに作ってそれで済むような起動の仕方であればShare Extensionを使えば良いですが、自作アプリ本体を起動しようと思ったらこれではうまくいきません。

ここで今回やりたいことを整理しておきます。

  • 他のアプリの共有シートから自分のアプリを起動したい
  • それも共有シート用の簡易画面ではなくアプリ本体を起動したい
  • その際に、他のアプリ側からテキストや画像を受け取りたい

要は他のアプリで表示されているテキストや画像を自分のアプリに取り込んで処理したいということです。他のアプリというのは例えばSafari、Twitterなどを想定しています。

試行錯誤

Share Extensionから何とかして自分のアプリを起動する

次のアイディアとして、Share Extensionからユニバーサルリンクや(既に非推奨になっていますが)カスタムURLスキームを使って自分のアプリを起動すれば良いのではないかということを思いつきます。

ところが、URLを開くための関数であるopen(_:completionHandler:)の説明を見ると、残念なことにTodayとiMessageのExtensionからしかopen(_:completionHandler:)を使用することができません。

ネット上を検索すると、それでも何とかして起動するためのハックが見つかりますが、裏技っぽいやり方でいつAppleに止められてもおかしくないような方法に見えます(試してみたところうまく動かなかったので、既にダメなのかもしれません)

Action Extensionを使う

先ほどの画像でアプリアイコンが並んでいるところがShare Extensionの部分ですが、その下の「リーディングリストに追加」とかの部分はAction Extensionという仕組みで拡張可能です。

ただ、これも結局はShare Extensionと同じ制約があって、自分のアプリ本体を起動することはできません。

解決策:ショートカットを使う

ショートカットとは

ショートカットというのはiOS標準(おそらくiOS13から?)でインストールされているアプリです。

自分でアクションを組み合わせて、ショートカットとして登録しておき、ワンタッチで呼び出すことができます。

4枚スクリーンショットを貼りましたが、ショートカット作成の+ボタンを押してから、App Storeアプリが提供している機能をショートカット内の1アクションとして選択するフローを表しています。2枚目でApp以外にスクリプティングというのもあるように、アプリの機能だけでなく条件分岐やループなども作ることができます。

このように、ショートカットというのは各アプリが提供している機能をスクリプト的に組み合わせて簡単に実行する仕組みです。

さらに、ショートカットの以下の設定画面を見るとわかるように、各アプリの共有シートに作成したショートカットを登録することができるのです。さらに、共有シートが共有対象としているコンテンツのタイプ(文字列とか画像とか)も選ぶことができます。

今回、文字列や画像を受け取りたいので共有シートタイプとして「テキスト」や「イメージ」を選ぶと実現できます。

自作アプリからショートカットに機能を提供する方法

では、どうやったら先ほどのApp Storeのように、アプリの機能を提供できるのでしょうか?
その仕組みがSiriKit ショートカットです。(どう検索したら良いかがわからなかったので、これにたどり着くまで結構苦労しました)

ここまでわかれば、あとは公式のリファレンスなり、実際に作った人のQiitaの記事なりを読んで実装すれば簡単、といけば良いのですが、意外とここからも大変でした。
SiriKitはその名前からわかる通り、基本的にはSiriを使った音声での呼びかけに対してアプリで応答するためのSDKですが、その枠組みでショートカット向けに機能提供することも可能になっています。世間の情報は割と音声寄りのものが多かったので、ショートカットを作るのに必要な部分が何なのか、今回自分がやりたいことを実現するためにどの範囲までやれば良いのかがなかなか読み取るのが難しい状況でした。そこで、今回やった内容をメモとして記録に残しておくことにしました。

具体的な作り方

SiriKitの公式ドキュメントが参考になるはずなのですが、今回使い方が若干特殊なのか、実際にやった内容は以下の記事の部分のみです。

  1. SiriショートカットとショートカットAppによるユーザー操作の追加
  2. application(_:continue:restorationHandler:)

Siriで音声応答するわけでもなく、共有シート用の別画面を用意するわけでもなく、直接本体アプリで文字列やファイルを受け取るので、Intents App Extensionもそれに関わるハンドル等の操作も不要でしたし、SiriKitの使用許可とかエンタイトルメントも不要でした。後で記述するように、ショートカットのドネートとかも行わず、ドメインとかボキャブラリとかも一切無視で大丈夫でした。

Intent definitionを作成する

まずはSiriショートカットとショートカットAppによるユーザー操作の追加に記載されているIntent Definition Fileを作成します。今回はテキストをアプリに取り込んで起動する機能と、画像をアプリに取り込んで起動する機能の2つを作りたかったので、Intentを2つ作成しました。パラメータはそれぞれString型、File型の1つずつを定義しました。重要なポイントとして、以下の設定を行いました。

  • ショートカットに関係の深そうな以下の設定を行いました
    • Custom intentセクションのIntent is user-configurable in the Shortcuts ap and Add to Siriをチェック
    • ParametersセクションのUser can edit value in Shortcuts, widgets, and Add to Siriをチェック
    • Shortcuts appのInput parameter, Key parameterは同じ値を設定しました(Intent1つにつき1つずつしかパラメータがなかったので)
  • Siriからの操作を行う想定はないので、Siriにしか関係なさそうな設定は外しました
    • Custom intentセクションのIntent is eligible for Siri Suggestionsのチェックを外す
    • ParametersセクションのSiri can ask for value when runのチェックを外す

また、結果を受け取る想定はなかったので、Responseはデフォルトのままとしました。

さて、公式ドキュメントではこの後ショートカットをドネートする手順が書かれていますが、これは何でしょうか?正確なところはよくわかりませんが、現時点の筆者の理解の仕方としては先ほど作ったIntentがオブジェクト指向でいうところのクラスのようなもので、ドネートはインスタンスを作成してOSに登録するような行為のようです。

ただ、今回の場合は文字列型とファイル型の1個ずつあれば良いだけで、それはIntent作成時に既にできているようだったので、ドネートの記事に記載されていることは特に行いませんでした。自分のアプリのこの画面に対してショートカットを作ったり削除したりしたいとか、動的に登録・削除をしたい場合はドネートという操作を行う必要があるようです。今回は固定で2個だったのでドネートは行いません。(Intent定義時点で暗黙に1個ドネートされているとかいう解釈なのかもしれませんが)

受け取った後の処理を記述する

Intent Definition Fileはビルドすると内部的にクラスファイルができるようで、定義したIntentのクラスが使えるようになります。これを利用して、application(_:continue:restorationHandler:)の説明を見ながらアプリがテキストや画像を受け取ってからの処理を記述しました。

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if #available(iOS 13.0, *) {
        if userActivity.activityType == String(describing: OpenBoardImageIntent.self) {
            // 画像を取得
            guard let intent = userActivity.interaction?.intent as? OpenBoardImageIntent else { return false }
            guard let data = intent.image?.data, let img = UIImage(data: data) else { return false }
            // 変数imgにUIImageが入ったので以下必要な処理を行う
            // :
            // :
            return true
        }
    }
    return false
}

アプリはSceneDelegateではなく、ApplicationDelegateを使用していたので上記関数内に実装しました。上記コードでOpenBoardImageIntentというのが定義したIntentの名前で、imageというのがFileパラメータになっています。

[application(_:continue:restorationHandler:)]にはユニバーサルリンク等からも呼び出されるので、まずuserActivityTypeで所定のIntentによる呼び出しかどうかを判断し、該当する場合はパラメータを取り出して以下必要な処理を行なっています。

上記はパラメータが画像だったのでちょっと複雑ですが、テキストの場合はそのままString型で取り出して処理できます。

ショートカットアプリでスクリプトを作成する

アプリ側の対応が終わったので、ショートカットアプリでスクリプトを作成します。
他のアプリで画像を選択した状態で共有シートを開いたら、作成したショートカットが表示されて、それを選ぶと自分のアプリに画像が飛ぶ、というイメージです。

ショートカットの詳細定義

共有シートに表示をオンにして共有シートに表示されるようにします。今回、画像を受け取るためのショートカットについては共有シートタイプをイメージ+URLにしました。URLも入れたのは、ブラウザで画像自体がリンクになっていてその先にオリジナルの画像があるようなケースではURLを入れておかないと取れなかったからです。

スクリプトの作成

基本的には受け取った画像をアプリに渡すだけなのですが、画像が渡ってこなかったときのフォールバックとして条件分岐をしています。イメージの個数(項目数)を数えて0より大きいかどうかを判断している部分がそれですが、1個以上渡ってきた場合は先ほど作成したIntentを呼んでアプリに渡すようにしています。
渡ってこなかった場合は写真アプリ内の写真を選択させて、それをアプリに渡すようにしました。こうしておくことでショートカットアプリ内で直接実行することもできます。

作ったショートカットを配布する

ショートカットアプリ内にはギャラリーというものがあって、既製のショートカットが配布されているようなので、AppStoreみたいに作ったショートカットを登録する仕組みがあるのかと思いましたが、どうもそれはないようでした。
ショートカットを他人に配布するためには、ショートカット自体を共有することで、セットアップするためのURLを生成することができます。

受け取る側の注意事項として、設定アプリで「信頼されていないショートカットを許可」というちょっとセキュリティ的に不安になる設定をしないとURLをタップしてもダウンロードできません。
さらに、この設定をオンにするためには、一度でもショートカットを使ったことがないといけないので、何かショートカットを実行してもらう必要があります。

ハマったこと

Intent Definition Fileでは、Storyboard等と同様に、stringsファイルによって他言語へのローカライズができるのですが、Intentを2つ作成したときに一部の文言の翻訳が行われなくなるという現象が発生しました。
これはその部分だけを切り出したアプリでも再現したので再現性はあると思うのですが、何故かしばらく(数日間)放置しておいたら直るという謎の直り方をしました。放置していた間は本当に何もしていないので、何故直ったのかは不明です。もし同じ状況に陥った場合は放置することで直るかもしれません(?)

最後に

最初の方にも書いた通り、やりたかったことは共有シートから自分のアプリを呼び出したいというだけなのですが、なかなか良い方法が見つからず、今回のショートカットの方法にしても一般的な使い方ではないのか情報が見つからずに苦労しました。折角なので、できたやり方をアウトプットしておきます。
ただ、もしかしたらもっと良いやり方があるのかもしれないので、ご存知の方がいらっしゃいましたら教えていただけると助かります。