iOS でジオフェンスがトリガーされない場合、原因はほぼ以下のいずれかです: アプリが「常に」権限を持っていない、地域を追加した時点でデバイスがすでにその地域内にあった、iOS が境界の越境を検出するのに半径が小さすぎる、20 地域の上限に達した、テストが十分に待機していなかった。iOS のジオフェンシングは機能しますが、越境の報告は独自のスケジュールで行われます。このガイドでは、地域監視がいつ発火するかを決定する方法と、Capacitor アプリのデバッグ方法について説明します @capgo/background-geolocation。iOS と Android でネイティブのジオフェンシングをサポートしています。
iOS の地域監視の仕組み
iOS では、ジオフェンスは CLCircularRegion Core Location に登録されたオブジェクトです。登録されると、OS が監視を行い、アプリは行いません。アプリがサスペンド状態や未実行状態であっても、iOS はセルタワー、Wi-Fi、GPS の組み合わせを使用して地域を追跡し、バッテリー消費を低く保ちます。
iOS がデバイスの境界越境を判断すると、必要かつ許可されている場合、バックグラウンドでアプリを起動し、enter または exit イベントをアプリに配信します。この判断を制御するいくつかのルールがあります:
- iOS は、デバイスが境界に触れた瞬間に越境を報告しません。Apple のガイダンスでは、デバイスが境界を越え、境界を最低限の距離だけ移動し、少なくとも約 20 秒間そこに留まった後にのみイベントが配信されるとされています。これにより、GPS のジッターによる誤発火を回避します。
- 地域監視の位置精度は、周囲の電波環境に依存します。Wi-Fiが密集した都市部では迅速ですが、郊外の道路では数分かかる場合があります。
- イベントは遷移時のみ発生します。監視開始時にすでに地域内にあることは、エントリとしてカウントされません。
Capacitor アプリでジオフェンシングを設定する
プラグインをインストールし、iOS のキーを設定します:
bun add @capgo/background-geolocation
bunx cap sync
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to detect when you arrive at a store.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need Always access to notify you when you arrive, even when the app is closed.</string>
次に、地域を登録します:
import { BackgroundGeolocation } from '@capgo/background-geolocation';
await BackgroundGeolocation.setupGeofencing({
url: 'https://api.example.com/v1/geofence-events',
headers: { Authorization: 'Bearer <token>' },
notifyOnEntry: true,
notifyOnExit: true,
payload: { userId: 'u-42' },
});
await BackgroundGeolocation.addGeofence({
identifier: 'store-paris-11',
latitude: 48.8606,
longitude: 2.3795,
radius: 150,
payload: { storeId: 'paris-11' },
});
await BackgroundGeolocation.addListener('geofenceTransition', (event) => {
console.log(`${event.transition} ${event.identifier}`, event.payload);
});
await BackgroundGeolocation.addListener('geofenceError', (event) => {
console.error('Geofence error', event.identifier, event.code, event.message);
});
setupGeofencing ジオフェンシングに必要な権限をデフォルトで要求します(requestPermissions デフォルトでは true). iOSでは「常に許可」を意味します。
次に、失敗するパターンを見ていきましょう。
1. 「常に許可」の権限がない
iOSのリージョン監視には「常に許可」の権限が必要です。「アプリの使用中のみ許可」では、アプリがバックグラウンドにある時や停止している時にイベントが届きません。これではジオフェンスの意味がありません。
リージョンを登録する前に権限を確認し、まずは独自のUIでリクエストの理由を説明しましょう:
const status = await BackgroundGeolocation.checkPermissions();
if (status.backgroundLocation !== 'always' && status.backgroundLocation !== 'granted') {
// Show your own screen explaining the feature, then:
const next = await BackgroundGeolocation.requestPermissions({
permissions: ['backgroundLocation'],
});
if (next.backgroundLocation === 'when_in_use' || next.backgroundLocation === 'denied') {
await BackgroundGeolocation.openSettings();
}
}
iOSでは「常に許可」へのアップグレードプロンプトがすぐに表示されない場合があります。「アプリの使用中のみ許可」を選んだユーザーには、後から2回目のプロンプトが表示されることがあります。「1回のみ許可」を選んだユーザーには一時的な権限が付与され、リセットされます。ジオフェンスがコア機能である場合は、アプリ起動時に毎回ステータスを確認し、「常に許可」でない場合は明確なバナーを表示するようにしてください。
2. デバイスがすでにリージョン内にいる
デバイスがすでにリージョン内にいる状態でジオフェンスを追加しても、iOSはenterイベントを送信しません。境界を越えていないからです。オフィス周辺にリージョンを設定してデスクでテストしている開発者は、この問題に頻繁に直面します。
アプリ側での対処法:リージョンを登録する際、現在の位置とリージョンを比較し、「すでに中にいる場合」の処理を自前で行ってください。
import { Geolocation } from '@capacitor/geolocation';
function distanceMeters(lat1: number, lon1: number, lat2: number, lon2: number) {
const R = 6371000;
const toRad = (d: number) => (d * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
const region = { identifier: 'store-paris-11', latitude: 48.8606, longitude: 2.3795, radius: 150 };
await BackgroundGeolocation.addGeofence(region);
const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true });
if (distanceMeters(pos.coords.latitude, pos.coords.longitude, region.latitude, region.longitude) < region.radius) {
handleAlreadyInside(region.identifier);
}
declare function handleAlreadyInside(id: string): void;
enterイベントをテストするには、半径の外まで歩くか車で移動して一度離れ、しばらく待ってから戻ってきてください。
3. 半径が小さすぎる
iOSは小さな半径も受け入れますが、100メートルを下回ると信頼性が急激に低下します。これはリージョン監視がWi-Fiや基地局の位置情報に依存しているためです。店舗の入り口付近に30メートルのリージョンを設定した場合、検知が遅れたり、顧客が立ち去った後に通知が来たり、あるいは全く検知されない可能性があります。
実用的な値:
| ユースケース | 推奨半径 |
|---|---|
| 店舗・レストランへの到着 | 100〜200 m |
| オフィス、キャンパス、倉庫 | 150〜300 m |
| 近隣地区または市街地 | 500 m 以上 |
ドアレベルの精度が必要なら、ユーザーが屋内に入った後は、大きめのジオフェンスとフォアグラウンド位置情報またはビーコンを組み合わせてください。Capgo には iBeacon プラグイン による屋内近接検出機能もあります。
4. 20 リージョンの上限に達した
iOS アプリは同時に最大 20 リージョンまでしか監視できません。さらに追加すると超過分の登録が失敗し、監視エラーが発生します。
使用する getMonitoredGeofences() 実際に登録されている内容を確認し、最寄りのリージョンのローリングセットを維持するには:
const { regions } = await BackgroundGeolocation.getMonitoredGeofences();
console.log(`Monitoring ${regions.length} regions`, regions);
よくあるパターン:
- すべての場所のリストはサーバーに保持してください。
- アプリ起動時、およびユーザーが大幅に移動した際(ユーザー周囲の大きな「リフレッシュ」領域を出た場合など)、最も近い19件の場所を取得します。
- 呼び出す
removeAllGeofences()その後、最も近い19件と、ユーザーを中心とした大きな領域を追加します。この大きな領域を出ることがリフレッシュのシグナルになります。
Androidではアプリごとに最大100個のジオフェンスが許可されるため、code Androidで動作するものが、この理由だけでiOSでは失敗することがあります。
5. テストが短すぎた、または静的すぎた
iOSは、デバイスが明確に境界を越えて反対側に留まった後にのみ遷移を報告します。時速50kmで150mの領域を通過すると、内部に滞在するのは約20秒です。それでは不十分な場合があります。
テスト時の注意点:
- 一度屋外に1分ほどいて、その後屋内に入って少なくとも2分ほどそのままにしてください。
- Wi-Fiをオンにした状態でテストしてください。Wi-Fiをオフにすると、ネットワークに接続していなくてもリージョン監視の精度が大幅に低下します。
- タイミングの検証にSimulatorを頼らないでください。XcodeのGPXルートでハンドラーの動作を確認した後、実機でテストしてください。
6. イベントは発火したが、JavaScriptが実行されなかった
リージョンイベントのためにiOSがバックグラウンドでアプリを再起動した際、ネイティブプラグインはイベントを受信しますが、Capacitor WebViewがロードされていないか、すぐにサスペンドされる可能性があります。 geofenceTransition リスナーはWebViewが生きている間しか発火しません。
そのため、 setupGeofencing は、 urlを受け付けます。プラグインはネイティブのcodeから、マージされた payloadを含む遷移をPOSTするため、JavaScriptが実行されなくてもバックエンドで受信できます。ヘッダーはセットアップ時に保持されるので再起動後も機能し、また updateHeaders().
配信はベストエフォート型で、リトライキューはありません。エンドポイントは高速かつ冪等(idempotent)にし、テスト中はすべてのリクエストをログに記録してください。
7. 位置情報サービスまたはバックグラウンドアプリリフレッシュがオフ
テストデバイスで以下を確認してください:
- 設定 > プライバシーとセキュリティ > 位置情報サービスがオンになっていること。
- アプリが「常に」に設定されていること。
- アプリで「正確な位置情報」がオンになっていること。オフだと、小さな地域では信頼性が低下します。
- 最初のテストでは低電力モードをオフにしてください。バックグラウンドアクティビティが減るためです。
8. 再起動とアプリの更新
iOSでは、監視対象のリージョンはアプリの起動をまたいで保持されます。デバイスの再起動後、システムが起動すると監視が再開されますが、アプリは起動時にリージョンを再同期する必要があります。期待する内容と getMonitoredGeofences() 比較し、不足しているものを再追加してください。アプリの更新後や、サーバーが場所のリストを変更した場合も同様です。
Androidでは、再起動後や、位置情報オフ・アプリデータのクリアなどいくつかの状況でジオフェンスが削除されるため、いずれにせよ起動時の再登録が必要です。両方のプラットフォームで行えば、ロジックをシンプルに保てます。
9. 強制終了
ユーザーがアプリスイッチャーからアプリを強制終了した場合、iOSはユーザーがアプリの実行を望んでいないという強いシグナルとみなします。強制終了後のリージョン再起動の挙動は、iOSのバージョンによって変化してきました。強制終了後にアプリが再起動された場合にのみ動作する機能は作らず、ユーザーが使用しているiOSバージョンでテストしてください。
デバッグチェックリスト
| 確認 | 方法 |
|---|---|
| 権限が「常に」の場合 | checkPermissions() を返します always または granted を backgroundLocation |
| リージョンが登録済み | getMonitoredGeofences() 識別子がリストに存在 |
| 20リージョン未満 | カウント元 getMonitoredGeofences() |
| 半径 ≥ 100 m | 更新されたコアと整合しているか、 addGeofence 呼び出し |
| まだ範囲内にいない | 登録時に現在地を比較 |
| エラーはログに記録されます | 〜をリッスンする geofenceError |
| バックエンドがイベントを受信 | 設定 url における setupGeofencing リクエストもログに記録 |
| 実際の移動によるテスト | 範囲から完全に出て、待機し、戻って留まる |
Androidにおける注意点
同じcodeがAndroidでは動作するのにiOSで動作しない場合、通常は20リージョン制限と「Always」要件が原因です。iOSでは動作するのにAndroidで動作しない場合は、次の点を確認してください: ACCESS_BACKGROUND_LOCATION manifestへの追加と設定 backgroundLocation: true における setupGeofencing。プラグインはGoogle Playでバックグラウンド位置情報の宣言が必要なため、この権限を自動で追加しません。
変更の反映方法
権限文字列とネイティブ設定の変更には新しいビルドが必要です。リージョンリスト、半径値、UIテキストはサーバー側で変更するか、Live Updateで配信できます。 Capgo Live Updatesenter/exitイベントではなく継続的なトラッキングについては、次を参照してください。 iOSがバックグラウンドの位置情報更新を停止する理由また、プラグインの ドキュメントも参照してください。 API の全文はこちら