ReactiveCocoa с асинхронными сетевыми запросами
Я создаю демонстрационное приложение и пытаюсь соответствовать ReactiveCocoa шаблон дизайна, насколько это возможно. Вот что делает приложение:
- Найдите местоположение устройства
- Всякий раз, когда изменяется ключ местоположения, выберите:
- Текущая погода
- Почасовой прогноз
- Ежедневный прогноз
Итак, порядок 1) место обновления 2) объединить все 3 погодных выборки. Я построил синглтон WeatherManager
, который предоставляет погодные объекты, информацию о местоположении и методы для ручного обновления. Этот синглтон соответствует протоколу CLLocationManagerDelegate
. Код местоположения очень простой, поэтому я оставляю его. Единственная реальная достопримечательность:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// omitting accuracy & cache checking
CLLocation *location = [locations lastObject];
self.currentLocation = location;
[self.locationManager stopUpdatingLocation];
}
Получение погодных условий очень похоже, поэтому я создал метод для создания RACSignal
для извлечения JSON из URL.
- (RACSignal *)fetchJSONFromURL:(NSURL *)url {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (! error) {
NSError *jsonError = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
if (! jsonError) {
[subscriber sendNext:json];
}
else {
[subscriber sendError:jsonError];
}
}
else {
[subscriber sendError:error];
}
[subscriber sendCompleted];
}];
[dataTask resume];
return [RACDisposable disposableWithBlock:^{
[dataTask cancel];
}];
}];
}
Это помогает мне сохранять мои методы красивыми и чистыми, поэтому теперь у меня есть 3 коротких метода, которые создают URL-адрес и возвращают RACSignal. Приятно, что я могу создавать побочные эффекты для анализа JSON и назначения соответствующих свойств (примечание: я использую Mantle здесь).
- (RACSignal *)fetchCurrentConditions {
// build URL
return [[self fetchJSONFromURL:url] doNext:^(NSDictionary *json) {
// simply converts JSON to a Mantle object
self.currentCondition = [MTLJSONAdapter modelOfClass:[CurrentCondition class] fromJSONDictionary:json error:nil];
}];
}
- (RACSignal *)fetchHourlyForecast {
// build URL
return [[self fetchJSONFromURL:url] doNext:^(NSDictionary *json) {
// more work
}];
}
- (RACSignal *)fetchDailyForecast {
// build URL
return [[self fetchJSONFromURL:url] doNext:^(NSDictionary *json) {
// more work
}];
}
Наконец, в -init
моего синглтона я установил наблюдателей RAC на место, так как каждый раз, когда меняется местоположение, я хочу получить и обновить погоду.
[[RACObserve(self, currentLocation)
filter:^BOOL(CLLocation *newLocation) {
return newLocation != nil;
}] subscribeNext:^(CLLocation *newLocation) {
[[RACSignal merge:@[[self fetchCurrentConditions], [self fetchDailyForecast], [self fetchHourlyForecast]]] subscribeError:^(NSError *error) {
NSLog(@"%@",error.localizedDescription);
}];
}];
Все работает отлично, но я обеспокоен тем, что я отклоняюсь от реактивного способа структурирования своих заданий и присвоений свойств. Я попытался выполнить последовательность с -then:
, но на самом деле не смог получить эту настройку, как бы мне хотелось.
Я также пытался найти чистый способ привязать результат асинхронной выборки к свойствам моего синглтона, но столкнулся с трудностями при работе с ними. Я не смог понять, как "расширить" выборку RACSignal
(обратите внимание: что идея -doNext:
появилась для каждого из них).
Любая помощь, очищающая это или ресурсы, будет действительно велика. Спасибо!
Ответы
Ответ 1
Для методов -fetch
кажется неуместным иметь значимые побочные эффекты, что заставляет меня думать, что ваш класс WeatherManager
объединяет две разные вещи:
- Сетевые запросы для получения последних данных
- Сохранение и представление данных с сохранением состояния
Это важно, потому что первая проблема - без гражданства, а вторая - почти полностью с точки зрения состояния. Например, в GitHub для Mac мы используем OCTClient для работы в сети, а затем сохраняем возвращаемые пользовательские данные в "постоянном диспетчере состояний" "singleton.
Как только вы сломаете это, я думаю, это будет легче понять. Менеджер штата может взаимодействовать с сетевым клиентом для запуска запросов, а затем менеджер штата может подписаться на эти запросы и применить побочные эффекты.
Прежде всего, сделайте методы -fetch…
безстоящими, переписав их для использования преобразований вместо побочных эффектов:
- (RACSignal *)fetchCurrentConditions {
// build URL
return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
return [MTLJSONAdapter modelOfClass:[CurrentCondition class] fromJSONDictionary:json error:nil];
}];
}
Затем вы можете использовать эти методы без учета состояния и вводить в них побочные эффекты, где это более уместно:
- (RACSignal *)updateCurrentConditions {
return [[self.networkClient
// If this signal sends its result on a background thread, make sure
// `currentCondition` is thread-safe, or make sure to deliver it to
// a known thread.
fetchCurrentConditions]
doNext:^(CurrentCondition *condition) {
self.currentCondition = condition;
}];
}
И, чтобы обновить их все, вы можете использовать +merge:
(как в вашем примере) в сочетании с -flattenMap:
для сопоставления значений местоположения с новым сигналом работы:
[[[RACObserve(self, currentLocation)
ignore:nil]
flattenMap:^(CLLocation *newLocation) {
return [RACSignal merge:@[
[self updateCurrentConditions],
[self updateDailyForecast],
[self updateHourlyForecast],
]];
}]
subscribeError:^(NSError *error) {
NSLog(@"%@", error);
}];
Или, чтобы автоматически отменять обновления при каждом изменении currentLocation
, замените -flattenMap:
на -switchToLatest
:
[[[[RACObserve(self, currentLocation)
ignore:nil]
map:^(CLLocation *newLocation) {
return [RACSignal merge:@[
[self updateCurrentConditions],
[self updateDailyForecast],
[self updateHourlyForecast],
]];
}]
switchToLatest]
subscribeError:^(NSError *error) {
NSLog(@"%@", error);
}];
(Исходный ответ от ReactiveCocoa/ReactiveCocoa # 786).
Ответ 2
Это довольно сложный вопрос, и я думаю, вам нужно всего лишь несколько указателей, чтобы выправить его.
- Вместо того, чтобы подписываться явно для местоположения, вы можете попытаться переформулировать с помощью
RACCommand
- Вы можете привязать сигнал к свойству с помощью макроса
RAC
RAC(self.currentWeather) = currentWeatherSignal;
- Этот учебник - отличный пример того, как вы можете эффективно осуществлять сетевое извлечение http://vimeo.com/65637501
- Постарайтесь поддерживать сигналы бизнес-логики и не настраивать их каждый раз, когда происходит событие. Видеоурок показывает очень элегантный способ сделать это.
Примечание: намеренно ли вы останавливать обновления местоположения в обновленном обратном вызове местоположения? Возможно, вы не сможете перезапустить его в будущих версиях iOS. (Это безумие, и я тоже бушует из-за этого.)