I'm building a demonstration app and am trying to conform to the ReactiveCocoa design pattern as much as possible. Here's what the app does:
So the order is 1) update location 2) merge all 3 weather fetches. I've built a WeatherManager
singleton that exposes weather objects, location information, and methods to manually update. This singleton conforms to the CLLocationManagerDelegate
protocol. The location code is very basic, so I'm leaving it out. The only real point of interest is this:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// omitting accuracy & cache checking
CLLocation *location = [locations lastObject];
self.currentLocation = location;
[self.locationManager stopUpdatingLocation];
}
Fetching the weather conditions is all very similar, so I've built a method to generate a RACSignal
for fetching JSON from a 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];
}];
}];
}
This helps me keep my methods nice and clean, so now I have 3 short methods that build a URL and return the RACSignal. The nice thing here is I can create side-effects to parse the JSON and assign the appropriate properties (note: I'm using Mantle here).
- (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
}];
}
Finally, in the -init
of my singleton, I set up the RAC observers on location, since every time the location changes I want to fetch and update the weather.
[[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);
}];
}];
Everything works just fine, but I'm concerned that I'm straying from the Reactive way to structure my fetches and property assignments. I tried doing sequencing with -then:
but wasn't really able to get that setup how I would like.
I was also trying to find a clean way to bind the result of an async fetch to the properties of my singleton, but ran into trouble getting that to work. I wasn't able to figure out how to "extend" the fetching RACSignal
s (note: that's where the -doNext:
idea came from for each of those).
Any help clearing this up or resources would be really great. Thanks!
It seems inappropriate for -fetch
methods to have meaningful side effects, which makes me think that your WeatherManager
class is conflating two different things:
This is important because the first concern is stateless, while the second is almost entirely stateful. In GitHub for Mac, for example, we use OCTClient to perform the networking, and then store the returned user data on a "persistent state manager" singleton.
Once you break it down like this, I think it'll be easier to understand. Your state manager can interact with the networking client to kick off requests, and then the state manager can subscribe to those requests and apply side effects.
First of all, let's make the -fetch…
methods stateless, by rewriting them to use transformations instead of side effects:
- (RACSignal *)fetchCurrentConditions {
// build URL
return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
return [MTLJSONAdapter modelOfClass:[CurrentCondition class] fromJSONDictionary:json error:nil];
}];
}
Then, you can use these stateless methods and inject side effects into them where it's more appropriate:
- (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;
}];
}
And, to update all of them, you can use +merge:
(like in your example) combined with -flattenMap:
to map from the location values to a new signal of work:
[[[RACObserve(self, currentLocation)
ignore:nil]
flattenMap:^(CLLocation *newLocation) {
return [RACSignal merge:@[
[self updateCurrentConditions],
[self updateDailyForecast],
[self updateHourlyForecast],
]];
}]
subscribeError:^(NSError *error) {
NSLog(@"%@", error);
}];
Or, to automatically cancel in-flight updates whenever currentLocation
changes, replace -flattenMap:
with -switchToLatest
:
[[[[RACObserve(self, currentLocation)
ignore:nil]
map:^(CLLocation *newLocation) {
return [RACSignal merge:@[
[self updateCurrentConditions],
[self updateDailyForecast],
[self updateHourlyForecast],
]];
}]
switchToLatest]
subscribeError:^(NSError *error) {
NSLog(@"%@", error);
}];
(Original reply from ReactiveCocoa/ReactiveCocoa#786).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With