Постановка задачи
В приложении необходимо отслеживать местоположение пользователя, когда приложение работает в фоновом режиме (с относительно приемлимой точностью), а также когда приложение активно (с высокой точностью).
Решение
Решение в лоб — использовать данные из коллбеков [CLLocationManagerInstance startUpdatingLocation] как в фоне, так и когда приложение активно. Первый и наиболее критичный недостаток данного решения — высокое энергопотребление (за несколько часов аккумулятор iPhone может полностью сесть). Второй — если приложение будет свернуто и 'убито', никаких апдейтов положения пользователя мы получить не сможем.
Для решения этих двух проблем, а также для того, чтобы сделать данное решение обособленным и не связанным с кодом основного приложения, напишем свой компонент, который будет использовать [CLLocationManagerInstance startUpdatingLocation] в активном режиме приложения и [CLLocationManagerInstance startMonitoringSignificantLocationChanges] в фоне. В компоненте будет два блока, которые будут исполнятся в зависимости от того, в каком состоянии находится приложение.
Определение местоположения пользователя
Foreground
Для активного приложение решение очевидно — нам нужно создать инстанс CLLocationManager и установить делегат, а затем в коллбеках обрабатывать полученные данные. Создадим объект-обертку:
#import <CoreLocation/CoreLocation.h>
typedef void(^locationHandler)(CLLocation *location);
@interface DLLocationTracker : NSObject <CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, copy) locationHandler locationUpdatedInForeground;
- (void)startUpdatingLocation;
- (void)stopUpdatingLocation;
@end
Блок locationUpdatedInForeground будет исполнятся при обновлении положения пользователя. Объект создается в контроллере, затем необходимо вызвать метод startUpdatingLocation для начала работы сервиса.
Background
Как уже упоминалось выше, есть два основных способа получать обновления координат в фоне:
- Выставить в *.plist приложения UIBackgroundModes = «location», и использовать [locationManager startUpdatingLocation] — очень энергозатратный, но точный способ;
- Использовать Significant Location Changes (>iOS 4.0) — энергоэффективно, использует данные сотовых сетей. Обновляется приблизительно раз в 10-15 минут, погрешность до 500 метров (определено опытным путем). Более подробно можно прочесть здесь.
Воспользуемся вторым подходом.
Обновим хедер нашего компонента:
#import <CoreLocation/CoreLocation.h>
typedef void(^locationHandler)(CLLocation *location);
@interface DLLocationTracker : NSObject <CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, copy) locationHandler locationUpdatedInForeground;
@property (nonatomic, copy) locationHandler locationUpdatedInBackground;
- (void)startUpdatingLocation;
- (void)stopUpdatingLocation;
- (void)endBackgroundTask;
@end
locationUpdatedInBackground блок будет вызываться, когда приложение получает апдейт координат в фоновом режиме.
endBackgroundTask — метод, который позволяет закончить задачу, выполняющуюся в фоне (рассмотрим позже).
Также в *.plist приложения нужно добавить пункт Required background modes = {App registers for location updates}.
Механизм Significant Location Changes позволяет получать апдейты местоположения даже в том случае, если приложение не запущено. Для этого нужно немножко переписать стандартный метод appDelegate applicationDidFinishLaunchingWithOptions:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {
self.locationTracker = [[DLLocationTracker alloc] init];
[self.locationTracker setLocationUpdatedInBackground:^(CLLocation *location) {
//тестовый блок, будет показывать local notification с координатами
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:15];
notification.alertBody = [NSString stringWithFormat:@"New location: %@", location];
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}];
[self.locationTracker startUpdatingLocation];
}
.....
}
UIApplicationLaunchOptionsLocationKey — ключ, который показывает, что приложение было запущено в ответ на поступившее событие об изменении местоположения.
Реализация компонента
При инициализации компонента создается инстанс CLLocationManager и объект устанавливается его делегатом, также подписываем его на нотификации об изменении состоянии прилоложения (активное/фоновое).
- (id)init {
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name: UIApplicationDidEnterBackgroundNotification object:nil];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
}
return self;
}
Дальше вызовем startUpdatingLocation:
- (void)startUpdatingLocation {
[self stopUpdatingLocation];
[self isInBackground] ? [self.locationManager startMonitoringSignificantLocationChanges] : [self.locationManager startUpdatingLocation];
}
В зависимости от состяния приложения активируется нужный сервис.
Все самое интересное происходит в коллбеке CLLocationManager:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
//фильтруем апдейты на основании минимального времени обновления и минимально дистанции
if (oldLocation && ([newLocation.timestamp timeIntervalSinceDate:oldLocation.timestamp] < kMinUpdateTime ||
[newLocation distanceFromLocation:oldLocation] < kMinUpdateDistance)) {
return;
}
if ([self isInBackground]) {
if (self.locationUpdatedInBackground) {
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
}];
self.locationUpdatedInBackground(newLocation);
[self endBackgroundTask];
}
} else {
//если приложение активно - выполняем этот блок
if (self.locationUpdatedInForeground) {
self.locationUpdatedInForeground(newLocation);
}
}
}
Для того, чтобы наше приложение могло что-либо сделать в фоне, необоходимо вызвать метод beginBackgroundTaskWithExpirationHandler и проинициализировать идентификатор bgTask (тип UIBackgroundTaskIdentifier). Каждый вызов этого метода должен быть сбалансирован вызовом endBackgroundTask:, что и происходит в [self endBackgroundTask]:
- (void)endBackgroundTask {
if (bgTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
}
Важным моментом является то, что блок locationUpdatedInBackground выполняется синхронно (мы можем себе это позволить, когда приложения в фоне), это может вызвать проблемы, если сворачивать/разворачивать приложение во время выполнения блока, а именно, если блок не выполнится в течение 10 секунд, приложение упадет.
Асинхронная отправка данных из фона
Для асинхронной отправки немого изменим код нашего компонента:
if ([self isInBackground]) {
if (self.locationUpdatedInBackground) {
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
}];
self.locationUpdatedInBackground(newLocation);
//[self endBackgroundTask]; - заканчивать таск будем по коллбекам нашей асинхронной операции в реализации блока
}
Блок locationUpdatedInBackground:
__weak DLLocationTracker *lc = self.locationTracker;
[self.locationTracker setLocationUpdatedInBackground:^ (CLLocation *location) {
//предположим, что у нас есть метод с completion и fail хендлерами для отправки местоположения
[self sendLocationToServer:location completion:^{
[lc endBackGroundTask];
} fail:^(NSError *fail) {
[lc endBackGroundTask];
}];
}];
Заключение
Подобный энергоэффективный способ используется во многих приложениях. Например, фича Radar в Forsquare. Код тестового приложения можно взять на github.