Как получать фоновое обновление местоположения каждые n минут в моем приложении iOS? - PullRequest
200 голосов
/ 14 июня 2011

Я ищу способ получать фоновое обновление местоположения каждые n минут в моем приложении iOS.Я использую iOS 4.3, и решение должно работать для iPhone без джейлбрейка.

Я пробовал / рассматривал следующие варианты:

  • CLLocationManager startUpdatingLocation/startMonitoringSignificantLocationChanges: Это работает в фоновом режиме, как и ожидалосьна основе настроенных свойств, но, по-видимому, невозможно заставить его обновлять местоположение каждые n минут
  • NSTimer: работает, когда приложение работает на переднем плане, но, кажется, не предназначенодля фоновых задач
  • Локальные уведомления: Локальные уведомления могут планироваться каждые n минут, но невозможно выполнить какой-либо код для получения текущего местоположения (без необходимости запуска приложения через уведомление).Этот подход также не выглядит чистым, поскольку это не то, для чего следует использовать уведомления.
  • UIApplication:beginBackgroundTaskWithExpirationHandler: Насколько я понимаю, это следует использовать для завершения некоторой работы в фоновом режиме.(также ограничено по времени), когда приложение перемещается в фоновый режим, а не реализует «длительные» фоновые процессы.

Как реализовать эти регулярные фоновые обновления местоположения?

Ответы [ 15 ]

114 голосов
/ 24 июня 2011

Я нашел решение для реализации этого с помощью форумов разработчиков Apple:

  • Укажите location background mode
  • Создайте NSTimer в фоновом режиме с UIApplication:beginBackgroundTaskWithExpirationHandler:
  • Когда n меньше , чем UIApplication:backgroundTimeRemaining, все будет работать нормально. Если n больше больше , location manager следует снова включить (и отключить), прежде чем останется время, чтобы избежать уничтожения фоновой задачи.

Это работает, потому что локация - это один из трех разрешенных типов фонового выполнения .

Примечание: я потерял некоторое время, протестировав это в симуляторе, где он не работает. Тем не менее, он отлично работает на моем телефоне.

53 голосов
/ 06 февраля 2015

Вкл. iOS 8/9/10 , чтобы обновлять фоновое местоположение каждые 5 минут:

  1. Перейти к проекту -> Возможности -> Фоновые режимы-> выберите Обновления местоположения

  2. Перейдите в Проект -> Информация -> добавьте ключ NSLocationAlwaysUsageDescription с пустым значением (или, возможно, любым текстом)

  3. Чтобы ваше местоположение работало, когда ваше приложение находится в фоновом режиме, и отправляйте координаты в веб-службу или делайте с ними что-нибудь каждые 5 минут, реализуйте это, как показано в коде ниже.

Я не используюлюбые фоновые задачи или таймеры.Я тестировал этот код на своем устройстве с iOS 8.1, которое несколько часов лежало на моем столе, а приложение работало в фоновом режиме.Устройство было заблокировано, и код работал все время правильно.

@interface LocationManager () <CLLocationManagerDelegate>
@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) NSDate *lastTimestamp;

@end

@implementation LocationManager

+ (instancetype)sharedInstance
{
    static id sharedInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
        LocationManager *instance = sharedInstance;
        instance.locationManager = [CLLocationManager new];
        instance.locationManager.delegate = instance;
        instance.locationManager.desiredAccuracy = kCLLocationAccuracyBest; // you can use kCLLocationAccuracyHundredMeters to get better battery life
        instance.locationManager.pausesLocationUpdatesAutomatically = NO; // this is important
    });

    return sharedInstance;
}

- (void)startUpdatingLocation
{
    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];

    if (status == kCLAuthorizationStatusDenied)
    {
        NSLog(@"Location services are disabled in settings.");
    }
    else
    {
        // for iOS 8
        if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)])
        {
            [self.locationManager requestAlwaysAuthorization];
        }
        // for iOS 9
        if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)])
        {
            [self.locationManager setAllowsBackgroundLocationUpdates:YES];
        }

        [self.locationManager startUpdatingLocation];
    }
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    CLLocation *mostRecentLocation = locations.lastObject;
    NSLog(@"Current location: %@ %@", @(mostRecentLocation.coordinate.latitude), @(mostRecentLocation.coordinate.longitude));

    NSDate *now = [NSDate date];
    NSTimeInterval interval = self.lastTimestamp ? [now timeIntervalSinceDate:self.lastTimestamp] : 0;

    if (!self.lastTimestamp || interval >= 5 * 60)
    {
        self.lastTimestamp = now;
        NSLog(@"Sending current location to web service.");
    }
}

@end
35 голосов
/ 24 июня 2011

Я сделал это в разрабатываемом приложении.Таймеры не работают, когда приложение находится в фоновом режиме, но приложение постоянно получает обновления местоположения.Я прочитал где-то в документации (я не могу найти его сейчас, я опубликую обновление, когда я это сделаю), что метод может быть вызван только в активном цикле выполнения, когда приложение находится в фоновом режиме.У делегата приложения есть активный цикл выполнения даже в bg, поэтому вам не нужно создавать свой собственный, чтобы это работало.[Я не уверен, что это правильное объяснение, но вот как я понял из того, что я прочитал]

Прежде всего, добавьте объект location для ключа UIBackgroundModes в info.plist вашего приложения.Теперь вам нужно запустить обновления местоположения в любом месте вашего приложения:

    CLLocationManager locationManager = [[CLLocationManager alloc] init];
    locationManager.delegate = self;//or whatever class you have for managing location
    [locationManager startUpdatingLocation];

Далее, напишите метод обработки обновлений местоположения, скажем, -(void)didUpdateToLocation:(CLLocation*)location, в делегате приложения.Затем реализуйте метод locationManager:didUpdateLocation:fromLocation из CLLocationManagerDelegate в классе, в котором вы запустили менеджер местоположения (поскольку мы установили делегат менеджера местоположения в 'self').В этом методе вы должны проверить, истек ли временной интервал, после которого вы должны обрабатывать обновления местоположения.Вы можете сделать это, сохраняя текущее время каждый раз.Если это время истекло, вызовите метод UpdateLocation из вашего делегата приложения:

NSDate *newLocationTimestamp = newLocation.timestamp;
NSDate *lastLocationUpdateTiemstamp;

int locationUpdateInterval = 300;//5 mins

NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (userDefaults) {

        lastLocationUpdateTiemstamp = [userDefaults objectForKey:kLastLocationUpdateTimestamp];

        if (!([newLocationTimestamp timeIntervalSinceDate:lastLocationUpdateTiemstamp] < locationUpdateInterval)) {
            //NSLog(@"New Location: %@", newLocation);
            [(AppDelegate*)[UIApplication sharedApplication].delegate didUpdateToLocation:newLocation];
            [userDefaults setObject:newLocationTimestamp forKey:kLastLocationUpdateTimestamp];
        }
    }
}

Это будет вызывать ваш метод каждые 5 минут, даже если ваше приложение находится в фоновом режиме.Imp: Эта реализация разряжает батарею, если точность данных о вашем местоположении не критична, вы должны использовать [locationManager startMonitoringSignificantLocationChanges]

Перед добавлением этого в ваше приложение, пожалуйста, прочитайте Руководство по программированию осведомленности о местоположении

24 голосов
/ 24 сентября 2012

Теперь, когда iOS6 - лучший способ иметь постоянно работающие службы определения местоположения, это ...

- (void)applicationWillResignActive:(UIApplication *)application
{
/*
 Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
 Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
 */

NSLog(@"to background");

app.isInBackground = TRUE;

UIApplication *app = [UIApplication sharedApplication];

// Request permission to run in the background. Provide an
// expiration handler in case the task runs long.
NSAssert(bgTask == UIBackgroundTaskInvalid, nil);

bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
    // Synchronize the cleanup call on the main thread in case
    // the task actually finishes at around the same time.
    dispatch_async(dispatch_get_main_queue(), ^{

        if (bgTask != UIBackgroundTaskInvalid)
        {
            [app endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }
    });
}];

// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // Do the work associated with the task.

    locationManager.distanceFilter = 100;
    locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
    [locationManager startMonitoringSignificantLocationChanges];
    [locationManager startUpdatingLocation];

    NSLog(@"App staus: applicationDidEnterBackground");
    // Synchronize the cleanup call on the main thread in case
    // the expiration handler is fired at the same time.
    dispatch_async(dispatch_get_main_queue(), ^{
        if (bgTask != UIBackgroundTaskInvalid)
        {
            [app endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }
    });
});

NSLog(@"backgroundTimeRemaining: %.0f", [[UIApplication sharedApplication] backgroundTimeRemaining]);

}

Только что проверил это так:

Я запустил приложение, отошел на второй план и переместился в машину на несколько минут. Затем я иду домой на 1 час и снова начинаю двигаться (не открывая снова приложение). Места начались снова. Затем остановился на два часа и начал снова. Все снова хорошо ...

НЕ ЗАБУДЬТЕ ИСПОЛЬЗОВАТЬ новые сервисы определения местоположения в iOS6

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{   
    CLLocation *loc = [locations lastObject];

    // Lat/Lon
    float latitudeMe = loc.coordinate.latitude;
    float longitudeMe = loc.coordinate.longitude;
}
13 голосов
/ 01 июня 2013

Для кого-то другого, у которого есть кошмар, выясните это. У меня есть простое решение.

  1. посмотрите этот пример с raywenderlich.com -> есть пример кода, он отлично работает, но, к сожалению, нет таймера во время фонового расположения это будет работать бесконечно.
  2. Добавить таймер с помощью:

    -(void)applicationDidEnterBackground {
    [self.locationManager stopUpdatingLocation];
    
    UIApplication*    app = [UIApplication sharedApplication];
    
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    
     self.timer = [NSTimer scheduledTimerWithTimeInterval:intervalBackgroundUpdate
                                                  target:self.locationManager
                                                selector:@selector(startUpdatingLocation)
                                                userInfo:nil
                                                 repeats:YES];
    
    }
    
  3. Только не забудьте добавить «Регистры приложений для обновлений местоположения» в info.plist.

7 голосов
/ 06 июля 2016

Вот что я использую:

import Foundation
import CoreLocation
import UIKit

class BackgroundLocationManager :NSObject, CLLocationManagerDelegate {

    static let instance = BackgroundLocationManager()
    static let BACKGROUND_TIMER = 150.0 // restart location manager every 150 seconds
    static let UPDATE_SERVER_INTERVAL = 60 * 60 // 1 hour - once every 1 hour send location to server

    let locationManager = CLLocationManager()
    var timer:NSTimer?
    var currentBgTaskId : UIBackgroundTaskIdentifier?
    var lastLocationDate : NSDate = NSDate()

    private override init(){
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
        locationManager.activityType = .Other;
        locationManager.distanceFilter = kCLDistanceFilterNone;
        if #available(iOS 9, *){
            locationManager.allowsBackgroundLocationUpdates = true
        }

        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.applicationEnterBackground), name: UIApplicationDidEnterBackgroundNotification, object: nil)
    }

    func applicationEnterBackground(){
        FileLogger.log("applicationEnterBackground")
        start()
    }

    func start(){
        if(CLLocationManager.authorizationStatus() == CLAuthorizationStatus.AuthorizedAlways){
            if #available(iOS 9, *){
                locationManager.requestLocation()
            } else {
                locationManager.startUpdatingLocation()
            }
        } else {
                locationManager.requestAlwaysAuthorization()
        }
    }
    func restart (){
        timer?.invalidate()
        timer = nil
        start()
    }

    func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
        switch status {
        case CLAuthorizationStatus.Restricted:
            //log("Restricted Access to location")
        case CLAuthorizationStatus.Denied:
            //log("User denied access to location")
        case CLAuthorizationStatus.NotDetermined:
            //log("Status not determined")
        default:
            //log("startUpdatintLocation")
            if #available(iOS 9, *){
                locationManager.requestLocation()
            } else {
                locationManager.startUpdatingLocation()
            }
        }
    }
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        if(timer==nil){
            // The locations array is sorted in chronologically ascending order, so the
            // last element is the most recent
            guard let location = locations.last else {return}

            beginNewBackgroundTask()
            locationManager.stopUpdatingLocation()
            let now = NSDate()
            if(isItTime(now)){
                //TODO: Every n minutes do whatever you want with the new location. Like for example sendLocationToServer(location, now:now)
            }
        }
    }

    func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
        CrashReporter.recordError(error)

        beginNewBackgroundTask()
        locationManager.stopUpdatingLocation()
    }

    func isItTime(now:NSDate) -> Bool {
        let timePast = now.timeIntervalSinceDate(lastLocationDate)
        let intervalExceeded = Int(timePast) > BackgroundLocationManager.UPDATE_SERVER_INTERVAL
        return intervalExceeded;
    }

    func sendLocationToServer(location:CLLocation, now:NSDate){
        //TODO
    }

    func beginNewBackgroundTask(){
        var previousTaskId = currentBgTaskId;
        currentBgTaskId = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
            FileLogger.log("task expired: ")
        })
        if let taskId = previousTaskId{
            UIApplication.sharedApplication().endBackgroundTask(taskId)
            previousTaskId = UIBackgroundTaskInvalid
        }

        timer = NSTimer.scheduledTimerWithTimeInterval(BackgroundLocationManager.BACKGROUND_TIMER, target: self, selector: #selector(self.restart),userInfo: nil, repeats: false)
    }
}

Я запускаю отслеживание в AppDelegate следующим образом:

BackgroundLocationManager.instance.start()
6 голосов
/ 14 июня 2011

К сожалению, все ваши предположения кажутся правильными, и я не думаю, что есть способ сделать это.В целях экономии заряда аккумулятора службы определения местоположения iPhone основаны на движении.Если телефон находится в одном месте, он невидим для служб определения местоположения.

CLLocationManager будет вызывать locationManager:didUpdateToLocation:fromLocation: только тогда, когда телефон получит обновление местоположения, что происходит только в том случае, если одна из трех служб определения местоположения (сотоваябашня, gps, wifi) воспринимает изменения.

Несколько других вещей, которые могут помочь в разработке дальнейших решений:

  • Запуск и остановка служб вызывают вызов метода делегата didUpdateToLocation, но newLocationможет иметь старую временную метку.

  • Мониторинг региона может помочь

  • При работе в фоновом режиме, имейте в виду, что онможет быть трудно получить «полную» поддержку LocationServices, одобренную Apple.Из того, что я видел, они специально разработали startMonitoringSignificantLocationChanges в качестве альтернативы с низким энергопотреблением для приложений, которые нуждаются в фоновой поддержке местоположения, и настоятельно рекомендуют разработчикам использовать это, если приложение не нуждается в этом.

Удачи!

ОБНОВЛЕНИЕ: Эти мысли уже устарели.Похоже, что у людей есть успех с ответом @wjans, выше.

5 голосов
/ 13 января 2016
if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
    [self.locationManager setAllowsBackgroundLocationUpdates:YES];
}

Это необходимо для фонового отслеживания местоположения начиная с iOS 9.

5 голосов
/ 09 июля 2014

Я написал приложение, используя службы определения местоположения, приложение должно отправлять местоположение каждые 10 секунд. И это сработало очень хорошо.

Просто используйте метод " allowDeferredLocationUpdatesUntilTraveled: timeout ", следуя документу Apple.

То, что я сделал:

Обязательно: Зарегистрировать фоновый режим для обновления Местоположение.

1. Создайте LocationManger и startUpdatingLocation, с accuracy и filteredDistance как хотите:

-(void) initLocationManager    
{
    // Create the manager object
    self.locationManager = [[[CLLocationManager alloc] init] autorelease];
    _locationManager.delegate = self;
    // This is the most important property to set for the manager. It ultimately determines how the manager will
    // attempt to acquire location and thus, the amount of power that will be consumed.
    _locationManager.desiredAccuracy = 45;
    _locationManager.distanceFilter = 100;
    // Once configured, the location manager must be "started".
    [_locationManager startUpdatingLocation];
}

2. Чтобы приложение продолжало работать вечно, используя allowDeferredLocationUpdatesUntilTraveled:timeout метод в фоновом режиме, необходимо перезапустить updatingLocation с новым параметром, когда приложение переходит в фоновый режим, например:

- (void)applicationWillResignActive:(UIApplication *)application {
     _isBackgroundMode = YES;

    [_locationManager stopUpdatingLocation];
    [_locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
    [_locationManager setDistanceFilter:kCLDistanceFilterNone];
    _locationManager.pausesLocationUpdatesAutomatically = NO;
    _locationManager.activityType = CLActivityTypeAutomotiveNavigation;
    [_locationManager startUpdatingLocation];
 }

3. Приложение получает обновленные местоположения в обычном режиме с locationManager:didUpdateLocations: обратным вызовом:

-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
//  store data
    CLLocation *newLocation = [locations lastObject];
    self.userLocation = newLocation;

   //tell the centralManager that you want to deferred this updatedLocation
    if (_isBackgroundMode && !_deferringUpdates)
    {
        _deferringUpdates = YES;
        [self.locationManager allowDeferredLocationUpdatesUntilTraveled:CLLocationDistanceMax timeout:10];
    }
}

4. Но вы должны обработать данные, затем locationManager:didFinishDeferredUpdatesWithError: обратный вызов для вашей цели

- (void) locationManager:(CLLocationManager *)manager didFinishDeferredUpdatesWithError:(NSError *)error {

     _deferringUpdates = NO;

     //do something 
}

5. ПРИМЕЧАНИЕ: Я думаю, что мы должны сбрасывать параметры LocationManager каждый раз, когда приложение переключается между режимом фона / форпоста.

4 голосов
/ 04 февраля 2014

Я использовал метод xs2bush для получения интервала (используя timeIntervalSinceDate) и немного его расширил. Я хотел убедиться, что я получаю требуемую точность, которая мне нужна, а также что я не разряжаю батарею, поддерживая радио GPS более чем необходимо.

Я постоянно работаю с настройками:

locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
locationManager.distanceFilter = 5;

это относительно низкий расход заряда батареи. Когда я готов получить мое следующее периодическое чтение местоположения, я сначала проверяю, находится ли местоположение в пределах моей желаемой точности, если это так, я затем использую местоположение. Если это не так, я увеличу точность с помощью этого:

locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
locationManager.distanceFilter = 0;

Получите мое местоположение, а затем, как только у меня будет это местоположение, я снова уменьшу точность, чтобы минимизировать разрядку батареи. Я написал полный рабочий пример этого, а также я написал исходный код для серверной стороны, чтобы собрать данные о местоположении, сохранить их в базе данных и позволить пользователям просматривать данные GPS в режиме реального времени или получать и просматривать ранее сохраненные маршруты. У меня есть клиенты для iOS, Android, Windows Phone и Java Me. Все клиенты изначально написаны, и все они работают правильно в фоновом режиме. Проект MIT лицензирован.

Проект iOS предназначен для iOS 6 с использованием базового SDK iOS 7. Вы можете получить код здесь .

Пожалуйста, сообщите о проблеме на github, если вы обнаружите какие-либо проблемы с ней. Спасибо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...