アプリ内に「LINE で開く」のようなボタンを置いた夜のことです。Android の実機では、アプリが入っていれば開き、入っていなければストアへ飛びます。ところが iOS のシミュレータでも実機でも、Linking.canOpenURL が必ず false を返し、毎回ストアのほうへ流れてしまいました。LINE は確かに端末に入っているのに、です。
コードを疑って 1 時間ほど過ごしましたが、原因はコードの外にあったのだと気づいたのは、Info.plist を開いたときでした。Info.plist に、調べたいスキームを宣言していなかっただけだったのです。
最初にお伝えしたいのは、canOpenURL が false を返すことと、openURL が失敗することは別の話だという点です。ここを分けて考えると、切り分けがぐっと楽になります。
false になる原因は、ほとんど 2 つに絞れます
Apple のドキュメントでは、iOS 9.0 以降にリンクしたアプリが canOpenURL に渡すスキームは、Info.plist の LSApplicationQueriesSchemes に宣言しておく必要があるとされています。宣言のないスキームは、対応アプリが入っていても常に false です。
もう一つは、端末にそのスキームを扱うアプリが本当に入っていない場合です。こちらも false ですが、見た目は同じなので区別がつきません。
切り分けの順番は次のとおりです。
| 手順 | 確かめること | わかること |
|---|---|---|
| 1 | Info.plist に該当スキームがあるか | 無ければ宣言漏れが確定します |
| 2 | スキームの綴りと「://」の有無 | 宣言するのはスキーム名だけです |
| 3 | prebuild し直したか | app.json を変えても、ネイティブ側に反映されていないことがあります |
| 4 | Expo Go で試していないか | Expo Go の Info.plist が使われるため、自分の宣言は効きません |
手順 4 は私が最初にはまった点です。Expo Go で確認している限り、app.json にどれだけ書いても結果は変わりません。開発ビルド(development build)で試す必要があります。
Expo の app.json での書き方
Expo の公式ガイドでは、ios.infoPlist に LSApplicationQueriesSchemes を書く形が示されています。
{
"expo": {
"ios": {
"infoPlist": {
"LSApplicationQueriesSchemes": ["line", "twitter"]
}
}
}
}何を解決するかというと、「調べたいスキームを OS に申告する」ことです。line:// ではなく line と書くのは、plist に入れるのがスキーム名だけだからです。
アプリ側のコードは、判定と遷移を分けて書いておくと後で直しやすくなります。
import { Linking } from 'react-native';
async function openOrFallback(appUrl: string, storeUrl: string) {
try {
const installed = await Linking.canOpenURL(appUrl);
await Linking.openURL(installed ? appUrl : storeUrl);
} catch (e) {
// Android では判定そのものが reject されることがある
await Linking.openURL(storeUrl);
}
}try/catch を入れているのは、Expo のドキュメントに、Android で確認できない場合は Promise が reject されると書かれているからです。ここで落ちて画面が固まるより、ストアへ流すほうが読者に親切だと私は考えています。
Android 11 以降は、宣言の置き場所が違います
Android には Info.plist はありません。パッケージの可視性の仕組みで、AndroidManifest の queries に調べたいアプリやインテントを書きます。Expo のガイドでは、app.json ではなく config plugin で manifest.queries を足す例が示されています。iOS の感覚で app.json に書いても、Android 側は変わらないのです。
iOS 27 では、宣言できる数が 25 件になります
ここからが、今回いちばん書き残したかったことです。Apple のドキュメントによると、iOS 15 以降にリンクしたアプリは LSApplicationQueriesSchemes を 50 件まで、iOS 27 以降にリンクしたアプリは 25 件までに制限されます。
SNS の共有先を何十個も判定するような作りだと、Xcode 27 でビルドし直したときに、後ろのほうの宣言が効かなくなるおそれがあります。対策は次の 3 つです。
- 本当に判定が要るスキームだけを残します(共有先なら、よく使われる上位だけです)
- 判定が要らない箇所では
canOpenURLを呼ばず、openURLを直接呼びます(openURLは宣言に縛られません) - 可能なら、カスタムスキームでなく universal link に寄せます(アプリが無ければブラウザが開きます)
私自身は、個人開発の壁紙アプリで「判定してから出し分ける」箇所を棚卸しし、本当に必要なものだけを残す方向にしています。
審査との関係
canOpenURL で他アプリの有無を探る使い方は、必要以上に多くのスキームを並べると、審査で用途を聞かれる可能性があります。宣言するのは、画面上の導線として本当に使うものだけに絞っておくのが無難かもしれません。ビルドが審査で止まるときの見方は、空白画面で審査に落ちたときのチェックリストにまとめています。
iOS 側のネイティブ設定をいじるときは、prebuild の挙動も確認が要ります。AppDelegate まわりで詰まった場合は、prebuild で AppDelegate のエラーが出るときも参考になります。Android 側の manifest を見直すなら、依存ライブラリの manifest 監査もあわせてご覧ください。
判定は最小限に、遷移は openURL で。 この線引きだけは、スキームの数が増えたときも守るようにしています。
次にやることは 1 つです。いま app.json に並べているスキームのうち、canOpenURL で本当に判定している数を数えてみていただければと思います。25 を超えていなければ、まず慌てる必要はありません。