【问题标题】:ReactiveCocoa sequencing with async network requests带有异步网络请求的 ReactiveCocoa 排序
【发布时间】:2013-09-13 09:28:23
【问题描述】:

我正在构建一个演示应用程序,并试图尽可能地符合ReactiveCocoadesign 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: 进行排序,但并没有真正按照我的意愿进行设置。

我还试图找到一种干净的方法来将异步获取的结果绑定到我的单例的属性,但是在让它工作时遇到了麻烦。我无法弄清楚如何“扩展”获取 RACSignals(注意:这就是 -doNext: 的每个想法的来源)。

任何帮助解决这个问题或资源都会非常棒。谢谢!

【问题讨论】:

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


    【解决方案1】:

    -fetch 方法似乎不适合具有有意义的副作用,这让我认为您的 WeatherManager 类将两个不同的东西混为一谈:

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

    这很重要,因为第一个关注点是无状态的,而第二个关注点几乎完全是有状态的。例如,在 Mac 版 GitHub 中,我们使用 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的原始回复)。

    【讨论】:

      【解决方案2】:

      这是一个相当复杂的问题,我认为您只需要一些提示即可解决问题。

      1. 您可以尝试使用RACCommand 重新格式化,而不是显式订阅该位置
      2. 您可以使用RACRAC(self.currentWeather) = currentWeatherSignal; 将信号绑定到属性
      3. 本教程是一个很好的例子,说明如何以干净的方式实现网络获取http://vimeo.com/65637501
      4. 尽量保留您的业务逻辑信号,而不是在每次事件发生时都设置它们。视频教程展示了一种非常优雅的方法。

      备注:您是否有意在位置更新回调中停止位置更新?您可能无法在以后的 iOS 版本中重新启动它。 (这太疯狂了,我也因此而愤怒。

      【讨论】:

      • 您对位置更新逻辑有任何参考吗?我假设您指的是 iOS 7 中的某些内容,如果是这样,我完全不知道这一点。如果这是真的,我会爆炸。
      • 使用 iOS 7 检查,CLLocationManager 没有任何问题。在 iOS 6 和 7 中工作相同。好奇你的评论是关于什么的。
      • 我尚未确认,但在 WWDC 视频中提到,如果应用程序处于后台,则无法启动 GPS 位置更新。例如。在重大更新中,您可以轻松地在 iOS6 中启动 GPS。这在iOS7中理论上是不可能的。如果您可以确认它仍然可以从后台启动,那么他们已经更改了这项政策,这对我们所有人都有好处。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-25
      • 2021-11-29
      • 1970-01-01
      相关资源
      最近更新 更多