使用异步网络请求进行ReactiveCocoa排序

时间:2013-09-09 19:53:39

标签: ios objective-c design-patterns functional-programming reactive-cocoa

我正在构建一个演示应用,并尽可能地尝试与ReactiveCocoa design pattern保持一致。以下是该应用的功能:

  • 查找设备的位置
  • 每当位置键更改时,都会获取:
    • 当前天气
    • 每小时预测
    • 每日预测

所以顺序是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以从URL中获取JSON。

- (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:想法来自每个人的地方)。

任何帮助清理它或资源都会非常棒。谢谢!

2 个答案:

答案 0 :(得分:13)

-fetch方法似乎不适合产生有意义的副作用,这让我认为你的WeatherManager类正在混淆两个不同的东西:

  1. 获取最新数据的网络请求
  2. 该数据的有状态存储和显示
  3. 这很重要,因为第一个问题是无国籍,而第二个几乎完全是有状态的。例如,在GitHub for Mac中,我们使用OCTClient来执行网络连接,然后将返回的用户数据存储在“持久状态管理器”单例中。

    一旦你这样打破它,我认为它会更容易理解。您的州经理可以与网络客户端进行交互以启动请求,然后州经理可以订阅这些请求并应用副作用。

    首先,让-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的原始回复)。

答案 1 :(得分:2)

这是一个非常复杂的问题,我认为你只需要一些指示就可以理顺它。

  1. 您可以尝试使用RACCommand
  2. 重新制定,而不是明确订阅该位置
  3. 您可以使用RACRAC(self.currentWeather) = currentWeatherSignal;
  4. 将信号绑定到属性
  5. 本教程是一个很好的示例,说明如何以干净的方式实现网络提取http://vimeo.com/65637501
  6. 尝试保持您的业务逻辑信号,并且不会在每次事件发生时设置它们。视频教程展示了一种非常优雅的方法。
  7. 备注:您是否有意在位置更新的回调中停止位置更新?您可能无法在将来的iOS版本中重新启动它。 (这很疯狂,我也因为它而肆虐。