壁紙アプリの宣伝枠に「TikTok を開く」「X でシェアする」を追加した翌日、レビュアーから「iPhone で押しても何も起きない」という連絡が立て続けに 3 件届いたことがあります。シミュレーターでも開発用クライアントでも動いていたので、ストア配布ビルドだけで起きる症状の切り分けに半日かかりました。原因は驚くほど単純で、Info.plist に LSApplicationQueriesSchemes を一行も書いていなかっただけだったのです。
私自身は 2014 年から個人でアプリ開発をしてきましたが、Linking.canOpenURL まわりは iOS 9 以降に挙動が大きく変わり、AdMob やプッシュ通知のような派手な変更と違って静かに罠だけが増えていきました。Rork(あるいは Expo / React Native)でこれをきれいに直すには、コード側ではなく設定ファイル側に手を入れる必要があります。
まず症状の特徴を確かめる
似たような不具合に見えても、LSApplicationQueriesSchemes 起因かどうかは挙動でほぼ判別できます。
- シミュレーターと開発ビルドでは動くが、TestFlight / 本番ストア配布ビルドだけ動かない
canOpenURL("tiktok://...")がコールバックでfalseを返す- Xcode のコンソールに
-canOpenURL: failed for URL: "..." - error: "This app is not allowed to query for scheme tiktok"という警告が出る - Android の同じコードは問題なく動いている
最後の警告メッセージが出ていれば、もう原因は確定です。iOS 9 以降、canOpenURL は宣言していないスキームに対して常に false を返すようになりました。Apple がプライバシー保護として導入した制限で、未宣言のスキームを使って他アプリのインストール状況を探る挙動を防ぐためのものです。
openURL 自体は宣言なしでも実行できる場合がありますが、Rork のように「インストールされていなければウェブ版を開く」という分岐をしているコードは、canOpenURL が常に false になることで「インストールされていない」と誤判定されます。結果として、ユーザーは TikTok アプリを入れているのに、毎回 Safari のウェブ版に飛ばされる、という体験になります。
修正 1: app.json に ios.infoPlist.LSApplicationQueriesSchemes を追加する
Rork は Expo の app config を踏襲しているので、Info.plist を直接いじる必要はありません。プロジェクトルートの app.json(または app.config.ts)に以下のキーを足します。
{
"expo": {
"name": "MyApp",
"ios": {
"bundleIdentifier": "net.dolice.myapp",
"infoPlist": {
"LSApplicationQueriesSchemes": [
"tiktok",
"twitter",
"instagram",
"line",
"fb",
"fb-messenger",
"youtube",
"mailto",
"tel",
"sms"
]
}
}
}
}app.config.ts を使っている場合は次のようになります。
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "MyApp",
slug: "myapp",
ios: {
bundleIdentifier: "net.dolice.myapp",
infoPlist: {
LSApplicationQueriesSchemes: [
"tiktok",
"twitter",
"instagram",
"line",
"fb",
"fb-messenger",
"youtube",
"mailto",
"tel",
"sms",
],
},
},
};
export default config;mailto・tel・sms は iOS が標準で許可しているので必須ではないものの、ここに並べておくとレビュー時に整理しやすいので、私は一緒に書いてしまいます。
修正 2: 実際の URL Scheme を間違えない
ハマりやすいのが「アプリ名 ≠ Scheme 名」のケースです。X を開きたいときに "x" と書いても無効で、現状は依然として "twitter" を使います。LINE も "line" ではなく LINE 公式アカウントの友だち追加なら "https://line.me/R/ti/p/..."(Universal Link)を使うのが安全です。
代表的なスキーム名を一覧にしておきます。
- TikTok アプリ起動:
tiktok - X(旧 Twitter)アプリ起動:
twitter - Instagram プロフィール:
instagram - LINE 友だち追加(Universal Link 経由が推奨):
https://line.me/... - Facebook ページ:
fb - Facebook Messenger:
fb-messenger - YouTube アプリ:
youtubeまたはvnd.youtube - Google マップ:
comgooglemaps - Apple マップ:
maps - Spotify:
spotify - Slack:
slack
これ以外のアプリは公式ドキュメントで「URL Scheme」と検索するのがいちばん早いです。サードパーティが勝手にまとめているリストはバージョンで変わっているので、引用前に実機で canOpenURL を呼んで確認しています。
修正 3: EAS Build を再ビルドしないと反映されない
ここでつまずく方がとても多いので強調しておきます。app.json を編集しただけでは、開発中の Expo Go や既存の Development Build には反映されません。infoPlist は ネイティブビルドに焼き込まれる設定 なので、最低でも次のいずれかが必要です。
# 開発用 Dev Client を作り直す
eas build --profile development --platform ios
# 本番ビルドを作り直す
eas build --profile production --platform ios
# ローカル prebuild + 実機転送
npx expo prebuild --clean
npx expo run:ios --deviceexpo prebuild --clean を打つと ios/ ディレクトリが再生成されるので、独自に追加したファイルがある場合は事前に差分を退避しておきます。私は CI 上でビルドする運用なので、app.json の変更を push してから 15 分待つ、というのを習慣にしています。
修正 4: Linking.canOpenURL のフォールバックを書き直す
設定が入っても、コード側で「失敗時にウェブ版に飛ばす」処理がないと、URL Scheme が変わったり Apple の挙動が変わったりした瞬間に静かに壊れます。以下のように書いておくと、安全に運用できます。
// utils/openExternalApp.ts
import { Linking, Platform } from "react-native";
type AppLink = {
/** ネイティブアプリを開くための URL(Scheme またはユニバーサルリンク) */
appUrl: string;
/** インストールされていない、または開けない場合のウェブフォールバック */
webUrl: string;
};
export async function openExternalApp({ appUrl, webUrl }: AppLink): Promise<void> {
try {
const supported = await Linking.canOpenURL(appUrl);
if (supported) {
await Linking.openURL(appUrl);
return;
}
} catch (e) {
// iOS では LSApplicationQueriesSchemes 未宣言時に例外が出ることがある
// ログは送るが、ユーザー体験を止めないようウェブにフォールバックする
if (__DEV__) {
console.warn("[openExternalApp] canOpenURL threw", e);
}
}
await Linking.openURL(webUrl);
}
// 使い方
// await openExternalApp({
// appUrl: "tiktok://user?username=dolice",
// webUrl: "https://www.tiktok.com/@masaki.hirokawa",
// });try/catch を必ず外側に置く点が肝です。LSApplicationQueriesSchemes 未宣言のスキームに canOpenURL を投げると例外まで飛ぶことがあり、リリース後にクラッシュレポートで気づくことになりかねません。
修正 5: 50 件の上限に気づいておく
LSApplicationQueriesSchemes には 最大 50 件まで という Apple 側の制限があります。50 件を超えても審査でリジェクトされるわけではないものの、51 件目以降は黙って無視されます。50 件は多いように見えますが、ソーシャル・地図・決済・配車・出会い系まで網羅し始めると意外と早く到達します。
grep -c '"' app.json で要素数の目安は取れますが、私は次のような小さなチェックを package.json の scripts.predeploy に挟んでいます。
// scripts/check-ios-queries.mjs
import fs from "node:fs";
const cfg = JSON.parse(fs.readFileSync("app.json", "utf8"));
const list = cfg?.expo?.ios?.infoPlist?.LSApplicationQueriesSchemes ?? [];
if (list.length > 50) {
console.error(
`LSApplicationQueriesSchemes has ${list.length} entries (Apple limit is 50).`
);
process.exit(1);
}
const dup = list.filter((x, i) => list.indexOf(x) !== i);
if (dup.length > 0) {
console.error(`Duplicate scheme(s) detected: ${dup.join(", ")}`);
process.exit(1);
}
console.log(`OK: ${list.length} scheme(s).`);ストア配布の直前にこれが落ちることで、過去 2 回ほど審査直前のトラブルを未然に防げました。
それでも canOpenURL が false の時に疑う 3 つのこと
ここまでやってもなお false が返るときは、次の 3 点を順に確認します。
- ビルドにキャッシュが残っている:
eas buildを走らせる前に Xcode の DerivedData を消す、もしくはexpo prebuild --cleanを打ってios/を再生成します。Rork の場合は EAS のキャッシュをクリアするオプションを付けて再実行します。 - iOS 14 以降のローカルネットワーク許諾と混同している: 自社アプリ間連携でカスタムスキームを使う場合、ローカルネットワーク許諾ダイアログとは別物です。
Info.plistのNSAppTransportSecurityも同時に疑わず、まずLSApplicationQueriesSchemesだけに集中します。 - App Store Connect 上で古いビルドが配信されている: TestFlight の Internal Testing で「最新ビルド」が緑になっているか、ビルド番号がコードと一致しているかを必ず照合します。私は壁紙アプリの審査で、
app.jsonを直したのに古いビルドを Promote しただけで再提出してしまい、半日無駄にしたことがあります。
次にやるべきこと
Linking.canOpenURL の挙動を理解できると、ディープリンク経由のキャンペーン計測や、他アプリ連動のオンボーディングなど、ストアで差別化しやすい体験を安全に作れるようになります。今日のうちに、お使いの Rork プロジェクトの app.json を開き、外部アプリへ飛ばしているスキームが一つでも LSApplicationQueriesSchemes に登録されていないか確認してみてください。