I have the following classes and methods:
Class A
- (RACSignal *)createX
{
NSDictionary *parameters = #{};
return [[[[HTTPClient sharedClient] rac_POST:#"X/" parameters:parameters]
map:^id(OVCResponse *response) {
[self logResponse:response];
return response.result;
}] catch:^RACSignal *(NSError *error) {
return [RACSignal error:[self handleError:error]];
}];
}
Class B
- (void)requestData
{
[[self.myClassA createX]
subscribeNext:^(NSArray *results) {
DDLogDebug(#"response : %#", results);
}
error:^(NSError *error) {
[self.dataManager sendError:error];
}];
}
Class C
- (void)retrieveData
{
[self.myClassB requestData];
}
What is the best way to design requestData in Class B such that the results array can be accessed in Class C.
i.e.
Should I forward the Array some way using [array rac_sequence],
should I create a new signal inside requestData, should requestData return a RACSignal instead of void?
Any help or guidance would be greatly appreciated. Thanks.
I believe you want to use doNext instead of subscribeNext in Class B.
I'm not entirely clear on your use case here but I think you are mixing paradigms. RAC stuff is always asynchronous so in order to access the result of your network request synchronously you will have to store it in some way.
You could bind the result to a property on ClassB or you could use RACCommand, something like:
[[RACCommand alloc] initWithEnabled:RACObserve(self, valid) signalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//make network call
//send responseObject to subscriber
[subscriber sendNext:responseObject];
//[subscriber sendError:#NSError#] //send error if something went wrong
[subscriber sendCompleted];
return nil;
}] materialize];
}];
You can then subscribe to the executionSignals of the RACCommand which streams a RACSignal for each execution of the command which you have control over in the block described above.
So I think your options are:
RACCommand pattern, look into it a bit more
Bind result of network call to a property for synchronous access
Return a RACSignal as you describe upon calling the method
Possibly look into replay() or replayLast() here as you could then store a reference to the RACSignal and subscribe to it for access to its last value
Related
I would like to launch multiple independent networking request in parallel, and subscribe to when all requests are done (I don't care if they are completed or error).
For now, I used combineLatest. Thus the requests are made in parallel (as I want), but when a signal sends error, I got an error in the subscription.
I can't find an operator that allows me to launch multiple independent request in parallel, and listen to when all are done.
Sincerely
EDIT
In other cases (where I need to use the signals individually), I need to subscribe to the error. So I still need my signal to be able to sendError:.
My use case is this:
On the one hand, I have an app that can do a full synchronisation. This will launch all independent request. I need to know when all requests are done in order to dismiss the progressHUD. I don't care if my signals have done sendErroror sendCompleted.
But on the other hand, I have multiple buttons, where I can relaunch individually each request, and in this case, I need to tell the user if the signal succeded or failed.
I came up with an answer, using the catch operator:
I add the catch RAC operator to all my signal, that make a new [RACSignal empty]in case the signal fails:
NSArray* allSignals = #[signal1, signal2, signal3, signal4];
NSMutableArray* optionalSignals = [NSMutableArray array];
for (RACSignal* signal in allSignals) {
RACSignal* catchableSignal = [signal catch:^RACSignal *(NSError *error) {
return [RACSignal empty];
}];
[optionalSignals addObject:catchableSignal];
}
return [RACSignal combineLatest:optionalSignals];
This does not change the nature of my individual signals, and I benefit from the combineLatestfeatures.
EDIT
If anyone has a more elegant RAC way to handle the for...in loop, I am still interested.
You can use rac_sequence and map instead of a loop. Also, in your case using catchTo will work in the same way as catch while making code more concise:
NSArray* allSignals = #[signal1, signal2, signal3, signal4];
RACSequence *optionalSignals = [[allSignals rac_sequence] map:^id(RACSignal *signal) {
return [signal catchTo:[RACSignal empty]];
}];
return [RACSignal combineLatest:optionalSignals];
You can try this code:
- (void)getAllItemsWithCompletion:(void (^)())completion {
NSArray *signals = [self.allItems.rac_sequence map:^id(Item *item) {
return [[[self getForItemSignal:item] doCompleted:^{
//done
}] doError:^(NSError *error) {
//Change the button title to retry
}];
}].array;
[[RACSignal combineLatest:signals] subscribeCompleted:^{
NSLog(#"All tasks are done");
}];
}
- (RACSignal *)getForItemSignal:(Item *)item {
return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
[APIClient getFeeForItemUUID:item.UUID
success:^(Fee *fee) {
[subscriber sendNext:#(fee)];
[subscriber sendCompleted];
}
failure:^(NSError *error) {
[subscriber sendError:error];
[subscriber sendCompleted];
}];
}];
}
I'm using RACCommand for my UI button click event. I'm using MVVM architecture. In my ViewModel I have this:
#property (strong, nonatomic) RACCommand *executeRegistration;
Inside "init" I have this:
self.executeRegistration = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input)
{
return [self executeSearchSignal];
}];
Execute search signal is this:
- (RACSignal *)executeSearchSignal {
return [[[self.services insertUserRegistration]
registerUserName:self.userName]
logAll];
}
My "[self.services insertUserRegistration]
registerUserName:self.userName" is this:
#weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
Manager *manager = [Manager sharedManager];
manager._delegate = self;
RACSignal *successSignal =
[self rac_signalForSelector:#selector(manager:didSuccesWithoutError:)
fromProtocol:#protocol(ManagerDelegate)];
RACSignal *failSignal =
[self rac_signalForSelector:#selector(manager:didFailWithError:)
fromProtocol:#protocol(ManagerDelegate)];
[[successSignal map:^id(RACTuple *tuple)
{
return tuple.second;
}] subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
[[failSignal map:^id(RACTuple *tuple)
{
return tuple.second;
}] subscribeNext:^(id x) {
[subscriber sendError:x];
}];
[manager insertUserRegistration:name];
return nil;
}];
My ViewController has this in BindViewModel method:
[self.finishRegistrationButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// Log a message whenever we log in successfully.
[loginSignal subscribeCompleted:^{
NSLog(#"I'm here.");
}];
}];
self.finishRegistrationButton.rac_command = self.viewModel.executeRegistration;
With my "logAll" atribute I can see everything executing, the problem is it never goes into subsrcibeCompleted after everything is ok. I want to show error message if there is an error or perform segue if everything is ok. What am I doing wrong? Can you please explain how to to that properly? I'm stuck here for quite some time now.
I did it. RACCommand 'does not have subscribeError'. Signals sent does not include error events. There is special property 'errors'. In that property, every signal that sends error sends is as 'next'. so, solution is to use this:
[self.executeRegistration.executionSignals subscribeNext:^(RACSignal *signal){
[signal subscribeCompleted:^{
NSLog(#"Registered");
}];
}];
[self.executeRegistration.errors subscribeNext:^(id x) {
NSLog(#"Error");
}];
This is ok. There is no need to subscribeNext if you don't want every new value. This is actually really cool stuff, but I've read that it is too confusing why it is not sending errors as in classic implementation (it was for me :)). That will be included in 3.0 if I'm not mistaken.
self.finishRegistrationButton.rac_command.executionSignals returns a signal of signals, so you'll want to make use of switchToLatest, like so:
self.finishRegistrationButton.rac_command.executionSignals.switchToLatest
This function intercepts signals and switches to the latest received, sending next, errors and completes from that signal instead of the signal operated on. It's very handy for operations that send signals over time.
This code will only work for executionSignals that hold a single signal, if you want multiple targets then you'll need something a bit more complex. If you ever need to change your code to work that way, you might want to have a look at flattenMap:.
I am willing to prevent a RAC signal to send a next event message before some subsequents calls are completed.
Here is an example of how i have proceed so far:
- (RACSignal *) fetchNearbyDatasForLocation: (CLLocationCoordinate2D)coordinate {
RACSignal* finalSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:self.rawDatas];
[subscriber sendCompleted];
return nil;
}];
return [[[self.apiClient fetchNearbyDatasforLocation:coordinate]
flattenMap:^RACStream *(NSArray* datas) {
NSMutableArray* signals = [[NSMutableArray alloc] init];
self.rawDatas = datas;
for (SNPDataModel* data in datas) {
RACSignal* fetchExtraDataSignal = [self.apiClient fetchExtraDataInfoForData:data];
[signals addObject:fetchExtraDataSignal];
RAC(data, extraData) = fetchExtraDataSignal;
}
// will send a next message when all asynchronous call are completed
RACSignal* completedSignal = [RACSignal combineLatest:signals];
return completedSignal;
}]
flattenMap:^RACStream *(RACTuple* value) {
return finalSignal;
}];
}
Few explanation about my code here: I am trying to fetch an array of datas and send a signal with the same array when all fetchExtraDataSignal signals have completed (so the array should now have the extra data content at that time). Is there a better reactive way to achieve this? I would like to not rely on the property datas if that's possible.
Although I don't have self-confidence in understanding what you are meaning,
I recommend using "map", instead of the combination of "createSignal" and "flattenMap".
Like this.
- (RACSignal *) fetchNearbyDatasForLocation: (CLLocationCoordinate2D)coordinate {
return [[self.apiClient fetchNearbyDatasforLocation:coordinate]
flattenMap:^RACStream *(NSArray* datas)
{
/* ... */;
return [[RACSignal combineLatest:signals]
map:^(RACTuple *unused) { return datas; }]];
}];
}
And I strongly recommend don't use any mutable fields to communicate data among event handlers. That causes results of these become unpredictable when multiple signals are simultaneously working.
For some reason I am not getting the error message to come through. (I've simplified the code here to get straight to the point.)
// Send an error message
_loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendError:error]; // Pretend this is a real error
return nil;
}];
}];
// Subscribe to loginButton's returned signal
[_loginButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[loginSignal subscribeError:^(NSError *error) {
NSLog(#"A");
} completed:^{
NSLog(#"B");
}];
}];
This prints "B". Any idea why? If -sendError: is called on the subscriber, why does the completion block receive it?
As you've discovered, RACCommand automatically catches errors within executionSignals.
This is intended to be a convenience for operators like -flatten, -concat, and -switchToLatest, which would otherwise prematurely terminate if an error occurs on any of the inner signals.
If all you care about is knowing when an error occurs, you should use RACCommand.errors instead. If you want to know where the error originated, checking the error domain and code may be easier (or at least more intuitive) than subscribing to the error event of each inner signal.
Subscriptions-within-subscriptions, and even subscriptions in general, are something of a code smell in RAC. Even if you don't want to use errors, there are generally higher-level operators to accomplish what you want (like using -map: to apply a -catch: to each inner signal).
This seems to work (materialize it and dematerialize it) based on this suggestion.
// Send an error message
_loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendError:error]; // Pretend this is a real error
return nil;
}] materialize];
}];
// Subscribe to loginButton's returned signal
[_loginButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[[loginSignal dematerialize] subscribeError:^(NSError *error) {
NSLog(#"A");
} completed:^{
NSLog(#"B");
}];
}];
In ReactiveCocoa, if we chaining several dependent signals, we must use subscribeNext: for the next signal in the chain to receive the value previous signal produced (for example, a result of an asynchronous operation). So after a while, the code turns into something like this (unnecessary details are omitted):
RACSignal *buttonClickSignal = [self.logIn rac_signalForControlEvents:UIControlEventTouchUpInside];
[buttonClickSignal subscribeNext:^(UIButton *sender) { // signal from a button click
// prepare data
RACSignal *loginSignal = [self logInWithUsername:username password:password]; // signal from the async network operation
[loginSignal subscribeNext:^void (NSDictionary *json) {
// do stuff with data received from the first network interaction, prepare some new data
RACSignal *playlistFetchSignal = [self fetchPlaylistForToken:token]; // another signal from the async network operation
[playlistFetchSignal subscribeNext:^(NSDictionary *json) {
// do more stuff with the returned data
}];
// etc
}];
}];
This ever-increasing nesting does not look much better than the non-reactive example that is given in the documentation:
[client logInWithSuccess:^{
[client loadCachedMessagesWithSuccess:^(NSArray *messages) {
[client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
NSLog(#"Fetched all messages.");
} failure:^(NSError *error) {
[self presentError:error];
}];
} failure:^(NSError *error) {
[self presentError:error];
}];
} failure:^(NSError *error) {
[self presentError:error];
}];
Am I missing something? Is there a better pattern of chaining dependent work in ReactiveCocoa?
This is when the RACStream and RACSignal operators start really coming in handy. In your particular example, you can use -flattenMap: to incorporate results into new signals:
[[[buttonClickSignal
flattenMap:^(UIButton *sender) {
// prepare 'username' and 'password'
return [self logInWithUsername:username password:password];
}]
flattenMap:^(NSDictionary *json) {
// prepare 'token'
return [self fetchPlaylistForToken:token];
}]
subscribeNext:^(NSDictionary *json) {
// do stuff with the returned playlist data
}];
If you don't need the results from any step, you can use -sequenceMany: or -sequenceNext: instead for a similar effect (but for a clearer expression of intent).