地理围栏是很多LBS类应用的核心能力:外卖App在你快到家时提醒取餐,打卡类App在你进入办公区时自动签到,家长监护类App在孩子离开学校时立即推送警告。这些场景背后都依赖iOS提供的区域监控(Region Monitoring)能力,它由Core Location框架提供,系统会在App处于前台、后台甚至完全被终止的状态下持续监控指定地理区域,并在跨越边界时重新唤起App执行回调。本文将从权限配置讲起,一步步实现一个完整的围栏监控加本地通知推送方案。

一、区域监控的底层机制与权限配置
区域监控的工作原理是手机基站和Wi-Fi指纹辅助定位,而不是持续开GPS。系统会周期性地通过低功耗方式粗略判断设备位置,一旦发现设备可能进入或离开了某个半径100米以上的圆形区域,就精确确认一次位置并触发事件。正因为如此,区域监控对电量的消耗非常小,但它也带来一个硬性限制:被监控区域的半径不能小于100米,即便你传入更小的值,系统也会自动放大到100米。
要做好区域监控,第一步是配置定位权限。iOS把定位权限分为三档:whenInUse(仅前台可用)、always(始终可用)以及升级后的 When In Use 完整精确度方案。区域监控要求必须持有 Always 级别的授权,仅前台权限是无法注册围栏的。在Info.plist中需要添加 NSLocationAlwaysAndWhenInUseUsageDescription 和 NSLocationWhenInUseUsageDescription 两个描述键,值为向用户解释用途的文案,这段文案会直接展示在系统授权弹窗中,写清楚价值能显著提升授权率。
授权申请有一个容易被忽略的细节:不要一启动就请求 Always 权限。正确的做法是先请求 When In Use,等功能真正需要后台监控时,再调用 requestAlwaysAuthorization,此时系统会展示升级弹窗,用户接受度更高。请求代码如下:
#import <CoreLocation/CoreLocation.h>
@interface FenceManager () <CLLocationManagerDelegate>
@property (strong, nonatomic) CLLocationManager *locationManager;
@end
@implementation FenceManager
- (instancetype)init {
self = [super init];
if (self) {
_locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
// 先判断当前授权状态
CLAuthorizationStatus status = _locationManager.authorizationStatus;
if (status == kCLAuthorizationStatusNotDetermined) {
// 第一步:先申请前台权限
[_locationManager requestWhenInUseAuthorization];
}
}
return self;
}
// 授权状态发生变化时回调
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
CLAuthorizationStatus status = manager.authorizationStatus;
if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
// 第二步:在合适时机引导用户升级到 Always
[manager requestAlwaysAuthorization];
} else if (status == kCLAuthorizationStatusAuthorizedAlways) {
// 权限就绪,可以开始注册围栏
[self startMonitoringFences];
}
}
@end
需要强调的是,授权状态回调 locationManagerDidChangeAuthorization: 在iOS 14之后是唯一的授权变化通知入口,旧的 locationManager:didChangeAuthorization: 已经废弃。另外iOS 14引入了精确定位(Accuracy)开关,如果用户只给了模糊定位,围栏触发精度会明显下降,可以通过 accuracyAuthorization 属性检测并引导用户在设置中开启精确定位。
二、注册地理围栏并处理进入离开事件
注册围栏使用 CLCircularRegion 对象,它由圆心坐标和半径构成,同时可以设置一个唯一标识符和一个 notifyOnEntry、notifyOnExit 开关组合。系统对同时监控的区域数量有上限,一般设备限制为20个,超出的注册会失败,所以在设计时要做好围栏池管理策略:只保留用户当前位置附近最有价值的围栏,在位置变化时动态增删。
注册与回调的完整实现如下,进入围栏时推送一条欢迎通知,离开时推送提醒通知:
- (void)startMonitoringFences {
// 定义一个地理围栏:以某个门店坐标为圆心,半径200米
CLLocationCoordinate2D center = CLLocationCoordinate2DMake(31.2304, 121.4737);
CLCircularRegion *region = [[CLCircularRegion alloc]
initWithCenter:center
radius:200.0
identifier:@"store_fence_001"];
// 进入和离开都通知
region.notifyOnEntry = YES;
region.notifyOnExit = YES;
// 先检查设备是否支持区域监控
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
NSLog(@"当前设备不支持区域监控");
return;
}
[self.locationManager startMonitoringForRegion:region];
}
#pragma mark - CLLocationManagerDelegate
// 进入围栏回调
- (void)locationManager:(CLLocationManager *)manager
didEnterRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLCircularRegion class]]) {
[self sendLocalNotificationWithTitle:@"到达提醒"
message:[NSString stringWithFormat:@"您已进入 %@ 附近", region.identifier]];
}
}
// 离开围栏回调
- (void)locationManager:(CLLocationManager *)manager
didExitRegion:(CLRegion *)region {
[self sendLocalNotificationWithTitle:@"离开提醒"
message:[NSString stringWithFormat:@"您已离开 %@ 区域", region.identifier]];
}
// 围栏注册出错回调,务必实现便于排查
- (void)locationManager:(CLLocationManager *)manager
monitoringDidFailForRegion:(CLRegion *)region
withError:(NSError *)error {
NSLog(@"围栏 %@ 注册失败:%@", region.identifier, error.localizedDescription);
}
// 发送本地通知
- (void)sendLocalNotificationWithTitle:(NSString *)title message:(NSString *)message {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = title;
content.body = message;
content.sound = [UNNotificationSound defaultSound];
UNNotificationRequest *request =
[UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString]
content:content
trigger:nil]; // trigger为nil表示立即发送
[center addNotificationRequest:request withCompletionHandler:nil];
}
这里有几个实战要点值得展开。第一,进入围栏的判定默认带有缓冲语义:当你注册围栏时如果设备已经在围栏内部,系统不会立刻触发进入事件,只有从外部跨入时才会触发;反过来,App重启后系统会对已注册围栏做一次状态比对,如果发现设备此前在围栏内、现在不在了,会补发一次离开事件。第二,iOS会在触发进入离开事件时做防抖处理,避免在边界附近来回穿越导致通知轰炸,这个行为无法自定义,设计交互时要接受这个延迟的存在。第三,本地通知必须先请求通知权限,调用 requestAuthorizationWithOptions:,否则回调执行了用户也看不到任何提示。
另外可以通过 locationManager.monitoredRegions 查看当前所有被监控的围栏,用 stopMonitoringForRegion: 移除不再需要的围栏。管理围栏池时建议用本地数据库持久化所有围栏定义,每次位置更新时对比 monitoredRegions 与数据库,动态替换最邻近的一批,确保不超过20个的硬性上限。
三、后台唤醒与App被杀后的恢复处理
区域监控最大的价值在于后台唤醒能力。当App在前台时,事件回调正常执行;当App在后台时,系统同样会唤起App并给几秒钟的执行时间处理回调,在这几秒内你可以发通知、同步网络请求;当App被用户手动从任务切换器杀掉后,情况会分成两类——对于正常启动的App,区域事件仍会触发一次,系统会短暂唤起App进程执行 didEnterRegion 回调,之后进程再次挂起;但对于用户主动上滑杀掉的App,iOS默认不再唤起,这是系统的隐私设计,无法绕过,只能引导用户重新打开App。
要保证后台唤醒后能正常工作,必须把初始化逻辑写在App启动链路中,而不能依赖某个界面。因为后台唤醒时不会有界面加载,如果 CLLocationManager 是在某个 ViewController 里创建的,回调将无人接收。正确的架构是把位置管理封装成一个单例,在 AppDelegate 的 didFinishLaunchingWithOptions: 中就完成初始化和代理绑定:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 请求通知权限
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert |
UNAuthorizationOptionSound |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError *error) {
NSLog(@"通知授权结果:%d", granted);
}];
// 尽早初始化位置管理单例,确保后台唤醒时回调有人接收
[FenceManager sharedManager];
return YES;
}
后台唤醒后可用的时间非常短,通常只有10秒左右,因此回调里不要做重量级操作。如果确实需要发起网络请求,可以使用 UIApplication beginBackgroundTask 申请额外几十秒的后台执行时间,或者用后台任务队列(BGTaskScheduler)把耗时工作延后调度。还有一个常见坑:delegate 是弱引用,如果 CLLocationManager 的 delegate 对象被提前释放,回调会静默失效且没有任何报错,这也是建议用单例持有多一层强引用的原因。
四、测试技巧与常见问题排查
真机测试区域监控非常耗时,推荐使用Xcode自带的位置模拟功能。新建一个 GPX 文件,定义两个位置点,一个在围栏外、一个在围栏内,设置好坐标后选择模拟该GPX文件,App会在两个点之间平滑移动,从而触发进入和离开事件。GPX示例如下:
<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
<wpt lat="31.2280" lon="121.4700">
<name>围栏外</name>
</wpt>
<wpt lat="31.2304" lon="121.4737">
<name>围栏内</name>
</wpt>
</gpx>
排查问题时优先检查三个环节:一是授权状态,确认是 Always 而不是 WhenInUse;二是 monitoredRegions 里是否真的存在目标围栏,注册失败往往是半径小于100米或围栏总数超限;三是查看Xcode控制台中的 monitoringDidFailForRegion 错误信息。此外要记住,系统对同一区域的重复触发做了合并,短时间内反复进出只会产生一次事件,测试时如果发现没有新通知,可以删除重装App重置状态。
最后补充一点工程建议:如果你的业务需要区分大量围栏,或者需要围栏形状不是圆形,区域监控就不够用了,应该考虑结合显著位置变化(Significant Location Change)配合本地判断,或者直接接入服务端下发最近围栏集合的方案。对于大多数门店提醒、到家打卡这类场景,20个圆形围栏加上动态管理已经完全够用,而且功耗和稳定性都经过了系统层面的充分验证,是iOS上做位置触达的首选方案。
Core Location区域监控地理围栏修改时间:2026-09-13 12:00:49