当 iOS 上的地理围栏不触发时,原因几乎总是以下几种之一:应用没有“始终允许”权限、添加区域时设备已经在区域内、半径太小导致 iOS 检测不到跨越、应用达到了 20 个区域的上限,或者测试等待时间不够。iOS 地理围栏本身工作良好,但它会按自己的节奏上报跨越事件。本指南会说明区域监控如何决定何时触发,以及如何调试 Capacitor 应用,使用 @capgo/background-geolocation,其中包含 iOS 和 Android 上的原生地理围栏功能。
iOS 区域监控的工作原理
在 iOS 上,地理围栏是 CLCircularRegion 在 Core Location 中注册的对象。一旦注册,由操作系统而非你的 App 进行监控。即使你的 App 被挂起或未运行,iOS 仍会通过结合基站、Wi-Fi 和 GPS 的方式持续追踪区域,以保持低功耗。
当 iOS 判断设备越过边界时,会向 App 发送进入或退出事件,并在需要且允许的情况下在后台启动 App。以下规则决定了这一判断:
- iOS 不会在设备刚碰到边界时就报告越界。根据 Apple 的指南,设备必须越过边界、再移动至少一段距离,并停留至少约 20 秒,事件才会送达。这样可以避免因 GPS 抖动而误触发。
- 区域监控的定位精度取决于周围的无线信号。在 Wi-Fi 密集的城市中,响应可能很快;而在乡村道路上,可能需要几分钟。
- 事件仅针对状态转换。开始监控时已经位于区域内,不算进入事件。
在 Capacitor App 中设置地理围栏
安装插件并配置 iOS keys:
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. 设备已经在区域内
如果你在手机已处于该区域时添加地理围栏,iOS 不会发送“进入”事件。因为没有发生跨越边界的行为。开发团队在办公室周围设置区域并在工位上测试时,经常会遇到这个问题。
在应用中修复它:当你注册一个区域时,请对比当前位置与该区域,并自行处理“已在区域内”的情况。
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;
要测试“进入”事件,请走或开车离开半径范围很远,等待片刻,然后再回来。
3. 半径太小
iOS 虽然接受较小的半径,但当半径低于约 100 米时,可靠性会迅速下降,因为区域监控依赖于 Wi-Fi 和蜂窝网络定位。商店门口 30 米的区域可能会触发延迟、在客户离开后才触发,甚至根本不触发。
实用参考值:
| 使用场景 | 建议半径 |
|---|---|
| 到达商店或餐厅 | 100 至 200 米 |
| 办公室、园区、仓库 | 150 至 300 米 |
| 社区或城市区域 | 500 米及以上 |
如果需要门级精度,请将较大的地理围栏与前台定位结合使用,或者在用户进入后使用 beacon。Capgo 也有一个 iBeacon 插件 用于室内近距离感知。
4. 达到了 20 个区域的上限
iOS 应用一次最多只能监控 20 个区域。如果你添加更多区域,超出部分的注册将会失败,并导致监控错误。
使用 getMonitoredGeofences() 以查看实际注册的区域,并保持一组动态更新的最近区域:
const { regions } = await BackgroundGeolocation.getMonitoredGeofences();
console.log(`Monitoring ${regions.length} regions`, regions);
一种常见的模式:
- 将完整的地点列表保存在服务器上。
- 当应用启动,或者用户移动较远时(例如离开用户周围的一个大型“刷新”区域),获取最近的 19 个地点。
- 调用
removeAllGeofences(),然后添加这 19 个最近地点,再加上一个以用户为中心的超大区域。离开该大区域即是触发刷新的信号。
Android 每个应用允许多达 100 个地理围栏,因此仅凭这一点,在 Android 上运行正常的 code 可能会在 iOS 上失败。
5. 测试时间太短或过于静态
iOS 仅在设备明确跨越边界并停留在另一侧后才会报告状态转换。如果你以 50 km/h 的时速驶过一个 150 米的区域,在其中的时间仅约 20 秒。这可能不足以触发检测。
测试时:
- 先在室外停留一分钟,然后进入室内并至少停留几分钟。
- 测试时请开启 Wi-Fi。即使未连接任何网络,关闭 Wi-Fi 也会大幅降低区域监控的准确性。
- 不要依赖模拟器进行计时测试。请在 Xcode 中配合 GPX 路线来检查 handler,然后使用真机进行测试。
6. 事件已触发,但 JavaScript 未运行
当 iOS 因区域事件在后台重新启动应用时,原生插件会接收到事件,但 Capacitor WebView 可能尚未加载,或者很快就会再次进入挂起状态。该 geofenceTransition 监听器仅在 WebView 活跃时触发。
这就是为什么 setupGeofencing 接受一个 url。插件会将来自原生 code 的状态转换通过 POST 请求发送,包括你合并后的 payload,因此即使 JavaScript 从未运行,你的后端也能收到数据。Headers 会随配置持久化,因此在应用重启后依然有效,你还可以通过 updateHeaders().
交付采用尽力而为(best effort)模式,不设重试队列。请确保 endpoint 响应迅速且具备幂等性,并在测试期间记录所有请求。
7. 定位服务或后台 App 刷新已关闭
请在测试设备上检查以下设置:
- 设置 > 隐私与安全性 > 定位服务已开启。
- 您的 App 已设置为“始终”。
- 您的 App 已开启“精确位置”。若关闭此项,小范围区域的监测将不可靠。
- 首次测试时请关闭“低电量模式”。该模式会减少后台活动。
8. 设备重启与 App 更新
iOS 会在 App 多次启动之间保留监测区域。设备重启后,系统启动完成后监测会恢复,但您的 App 仍应在启动时重新同步区域:请将当前区域 getMonitoredGeofences() 与预期进行对比,并重新添加缺失的区域。在 App 更新或服务器更改地点列表时,也应执行相同操作。
在 Android 上,设备重启及其他情况(如关闭定位、清除 App 数据)会导致地理围栏被移除,因此 Android 端无论如何都需要在启动时重新注册。在两个平台上都执行此操作可以简化逻辑。
9. 强制退出
如果用户在 App 切换器中强制退出了 App,iOS 会将其视为用户不想运行该 App 的强烈信号。不同 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 要求声明后台位置权限。
发布变更
权限字符串和原生配置需要重新 build。区域列表、半径值和 UI 文本可以在服务端修改,或通过 Live Updates 下发 Capgo 实时更新。如果需要持续追踪而非仅监听进入/退出事件,请参阅 为什么 iOS 会停止后台位置更新,以及该插件的 文档 查看完整的 API。