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. (Это безумие, и я тоже бушует из-за этого.)