Лучший подход Reactive-Cocoa для написания CLLocationManagerDelegate, который будет нечасто получать местоположение

Задний план

Я действительно взволнован фреймворком ReactiveCocoa и его потенциалом, поэтому я решил, что собираюсь стиснуть зубы и написать свое первое приложение, используя его. В моем приложении я уже написал различные сервисы и делегаты, но теперь мне нужно их «Reactive-Cocoa-ise», чтобы я мог работать с фактической стороной GUI.

Тем не менее, чтобы лучше понять это, я пишу простой код, чтобы опробовать концепции. В этом случае написание оболочки для CLLocationManagerDelegate.

В реальном приложении вариант использования будет таким:

  • 1) Когда приложение загружается (viewDidLoad), затем 2) Попытка получить местоположение устройства с помощью
  • 2.1) если службы определения местоположения не включены, то
  • 2.1.1) проверить статус авторизации, и если разрешено startMonitoringSignificantLocationChanges,
  • 2.1.2) иначе вернуть ошибку
  • 2.2) иначе (услуги определения местоположения включены)
  • 2.2.1) если последнее местоположение менеджера местоположения было «недавним» (6 часов или меньше), вернуть это
  • 2.2.2) иначе startMonitoringSignificantLocationChanges
  • 3) при возврате локации (или сразу, или через locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations) то тоже stopMonitoringSignificantLocationChanges
  • 4) Если код, который вызывает LocationManagerDelegate, получает и получает ошибку, то запросите у делегата фиксированное значение «по умолчанию».
  • 5) Затем следующий фрагмент кода использует местоположение (выбранное или заданное по умолчанию) для запуска и выполнения множества вызовов сторонних сервисов (вызовы веб-сервисов и т. д.).

Вы можете увидеть тестовый набор по адресу https://github.com/ippoippo/reactive-core-location-test

Обеспокоенность

Привязь «работает» в том смысле, что она снимается и определяет местоположение. Тем не менее, я действительно обеспокоен тем, что делаю это правильно.

Рассмотрим этот код

RACSignal *fetchLocationSignal = [lmDelegate latestLocationSignal];

RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
    NSLog(@"Unable to fetch location, going to grab default instead: %@", error);
    CLLocation *defaultLocation = [lmDelegate defaultLocation];
    NSLog(@"defaultLocation = [%@]", defaultLocation);
    // TODO OK, so now what. I just want to handle the error and 
    // pass back this default location as if nothing went wrong???
    return [RACSignal empty];
}];

NSLog(@"Created signal, about to bind on self.locationLabel text");

RAC(self.locationLabel, text) = [[locationSignal filter:^BOOL(CLLocation *newLocation) {
    NSLog(@"Doing filter first, newLocation = [%@]", newLocation);
    return newLocation != nil;
}] map:^(CLLocation *newLocation) {
    NSLog(@"Going to return the coordinates in map function");
    return [NSString stringWithFormat:@"%d, %d", newLocation.coordinate.longitude, newLocation.coordinate.latitude];
}];

Вопросы

  • 1) Как обрабатывать ошибки? Я могу использовать catch, что затем дает мне возможность запросить у делегата местоположение по умолчанию. Но затем я застрял на том, как передать это местоположение по умолчанию в качестве сигнала? Или мне просто изменить метод defaultLocation, чтобы он возвращал RACSignal, а не CLLocation??
  • 2) Когда я возвращаю местоположение, это должно быть сделано как sendNext или sendCompleted? В настоящее время он закодирован как sendNext, но похоже, что его можно было бы сделать как sendCompleted.
  • 3) На самом деле, зависит ли ответ на этот вопрос от того, как я создаю делегата. Это тестовое приложение создает нового делегата каждый раз при загрузке представления. Это что-то, что я должен сделать, я должен сделать делегата синглтоном. Если это синглтон, то sendNext кажется правильным. Но если бы это означало, что я вместо этого перемещаю код, который у меня есть в latestLocationSignal в моем делегате, в метод инициализации?

Извиняюсь за бессвязный вопрос (вопросы), просто кажется, что у меня в голове крутится круг.


person ippoippo Software    schedule 28.02.2014    source источник


Ответы (3)


Обновление ответа выше до ReactiveCocoa 4 и Swift 2.1:

import Foundation
import ReactiveCocoa

class SignalCollector: NSObject, CLLocationManagerDelegate {

let (signal,sink) = Signal<CLLocation, NoError>.pipe()

let locationManager = CLLocationManager()

  func start(){
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.startUpdatingLocation()
  }

  func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

    for item in locations {

        guard let location = item as CLLocation! else { return }         
        sink.sendNext(location)
    }
}

И слушать события:

 var locationSignals : [CLLocation]  = []  
 signal.observeNext({ newLocation in
      locationSignals.append(newLocation)
    })
person scollaco    schedule 08.01.2016

Как обрабатывать ошибки? Я могу использовать catch, который затем дает мне возможность запросить у делегата местоположение по умолчанию.

Ты так близко. Используйте +[RACSignal return:] для создания сигнала, который просто отправляет ваше местоположение по умолчанию:

RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
    NSLog(@"Unable to fetch location, going to grab default instead: %@", error);
    CLLocation *defaultLocation = [lmDelegate defaultLocation];
    NSLog(@"defaultLocation = [%@]", defaultLocation);
    return [RACSignal return:defaultLocation];
}];

Когда я возвращаю местоположение, это должно быть сделано как sendNext или sendCompleted? В настоящее время он закодирован как sendNext, но похоже, что это будет сделано как sendCompleted.

Чтобы фактически отправить данные, вам потребуется использовать -sendNext:. Если это единственное, что посылает сигнал, то он также должен -sendCompleted после этого. Но если он будет продолжать отправлять местоположение по мере улучшения точности или изменения местоположения, то это еще не должно быть завершено.

В более общем случае сигналы могут отправлять любое количество nexts (0*), но могут завершать только ошибку или. Как только завершение или ошибка отправлены, сигнал готов. Он больше не будет отправлять значения.

На самом деле, зависит ли ответ на этот вопрос от того, как я создаю делегата. Это тестовое приложение создает нового делегата каждый раз при загрузке представления. Это что-то, что я должен сделать, я должен сделать делегата синглтоном. Если это синглтон, то sendNext кажется правильным. Но если бы это означало, что вместо этого я перемещаю код, который у меня есть в lastLocationSignal в моем делегате, в метод инициализации?

Я не уверен, что у меня достаточно контекста, чтобы ответить на этот вопрос, за исключением того, что синглтоны никогда не являются ответом;) Создание нового делегата каждый раз при загрузке представления кажется мне разумным.

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

person joshaber    schedule 28.02.2014
comment
Спасибо за это. +[RACSignal return:] подход кажется хорошим. Другой подход, который я пробовал, заключался в том, чтобы определить метод defaultLocation в моем делегате, чтобы вместо этого возвращать сигнал. Просто ваш способ кажется проще и достигает того же. - person ippoippo Software; 01.03.2014
comment
Кроме того, да, оцените, что это широкий вопрос, и поэтому на него труднее «ответить». Но это дало мне пищу для размышлений и экспериментов. Я еще не буду отмечать вопрос как завершенный, если у кого-то еще есть что добавить. (Я не уверен, что код на Github, github.com/ippoippo/reactive- core-location-test) поможет понять, чего я хочу добиться с помощью делегата.) - person ippoippo Software; 01.03.2014

Кажется, это очень просто в ReactiveCocoa 3.0 и быстро:

import ReactiveCocoa
import LlamaKit

class SignalCollector: NSObject, CLLocationManagerDelegate {

    let (signal,sink) = Signal<CLLocation, NoError>.pipe()

    let locationManager = CLLocationManager()

    func start(){
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.startUpdatingLocation()
    }

    func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
        for item in locations {
            if let location = item as? CLLocation {
                sink.put(Event.Next(Box(location))
            }
        }
    }

}

И везде, где вам нужно слушать события местоположения, используйте сигнал с наблюдателем:

var locationSignals : [CLLocation]  = []
signal.observe(next: { value in locationSignals.append(value)})

Обратите внимание, что, поскольку на данный момент это предварительная альфа-версия, синтаксис, вероятно, изменится.

person ohad serfaty    schedule 16.04.2015