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

rny*_*rom 10 design-patterns functional-programming objective-c ios reactive-cocoa

我正在构建一个演示应用程序,并尽可能地尝试符合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];
}
Run Code Online (Sandbox Code Playgroud)

获取天气条件非常相似,所以我构建了一个方法来生成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];
        }];
    }];
}
Run Code Online (Sandbox Code Playgroud)

这有助于我保持方法的美观和干净,所以现在我有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
    }];
}
Run Code Online (Sandbox Code Playgroud)

最后,在-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);
     }];
 }];
Run Code Online (Sandbox Code Playgroud)

一切都很好,但我担心我会从反应的方式偏离构建我的提取和属性分配.我尝试使用排序,-then:但实际上并不能按照我的意愿进行设置.

我也试图找到一种干净的方法将异步提取的结果绑定到我的单例的属性,但是遇到麻烦让它工作.我无法弄清楚如何"扩展"取物RACSignal(注意:这是-doNext:每个想法的来源).

任何帮助清除这些或资源将是非常好的.谢谢!

Jus*_*ers 13

-fetch对于具有有意义的副作用的方法来说似乎是不合适的,这让我觉得你的WeatherManager课正在混淆两个不同的东西:

  1. 网络请求获取最新数据
  2. 存储和呈现该数据的状态

这很重要,因为第一个问题是无国籍,而第二个几乎完全是有状态的.例如,在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];
    }];
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用这些无状态方法,并在其中更合适的地方注入副作用:

- (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;
        }];
}
Run Code Online (Sandbox Code Playgroud)

并且,要更新所有这些,您可以使用+merge:(如在您的示例中)结合使用-flattenMap:从位置值映射到新的工作信号:

[[[RACObserve(self, currentLocation)
    ignore:nil]
    flattenMap:^(CLLocation *newLocation) {
        return [RACSignal merge:@[
            [self updateCurrentConditions],
            [self updateDailyForecast],
            [self updateHourlyForecast],
        ]];
    }]
    subscribeError:^(NSError *error) {
        NSLog(@"%@", error);
    }];
Run Code Online (Sandbox Code Playgroud)

或者,要在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);
    }];
Run Code Online (Sandbox Code Playgroud)

(来自ReactiveCocoa/ReactiveCocoa#786的原始回复).