Chaining dependent signals in ReactiveCocoa - ios

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).

Related

Reactive Cocoa Legacy - How to handle multiple independent signals in parallel?

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];
}];
}];
}

ReactiveCoca Design Pattern for Forwarding / Chaining Signals

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

Using Reactivecocoa login button only triggered once

Here is my basic scenario:
I am making a simple login framework, so I got two text field, one for username and the other for password, and of course a login button.
Now I bind them with RACSignal like this:
RACSignal *validPasswordSignal = [passwordTextField.rac_textSignal map:^id(NSString *text)
{
return #([self isPasswordValid:text]);
}];
RACSignal *validUsernameSignal = [usernameTextField.rac_textSignal map:^id(NSString *text)
{
return #([self isUsernameValid:text]);
}];
And combine two signals (username and password) into one like this:
RACSignal *submitActiveSignal = [RACSignal combineLatest:#[validpasswordSignal, validUsernameSignal] reduce:^id(NSNumber *validPW, NSNumber *validUN)
{
return #(validPW.boolValue && validUN.boolValue);
}];
Of course I need a method to submit signals for username and password with request to server, so I have it like this:
-(RACSignal *)submitSignal
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
[[[RequestClass alloc] init] requestWithUsername:_usernameTextField.text.trim password:_passwordTextField.text.trim completionHandler:^(MyResult *results, NSURLResponse *response, NSError *error)
{
if (!error)
{
if (results.code == 0)
{
[subscriber sendNext:#(YES)];
[subscriber sendCompleted];
}
else
{
NSError *invalidError = [NSError errorWithDomain:MyErrorDomain code:MyErrorInvalidSigniture userInfo:#{#"NSLocalizedDescription": results.message}];
[subscriber sendError:invalidError];
}
}
else
{
[subscriber sendError:error];
}
}];
return nil;
}];
}
Then I need to let my login button catch the signal from subscriber's sendNext and sendError, so I did it like this:
//subscribeNext
[[[[_submitButton rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x)
{
_submitButton.enabled = NO;
}]
flattenMap:^RACStream *(id value)
{
return [self submitSignal];
}]
subscribeNext:^(NSNumber *signal)
{
_submitButton.enabled = YES;
BOOL success = signal.boolValue;
if (success)
{
[self successCallback];
}
else
{
//do sth
}
}];
//subscribeError
[[[[_submitButton rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x)
{
_submitButton.enabled = NO;
}]
flattenMap:^RACStream *(id value)
{
return [self submitSignal];
}]
subscribeError:^(NSError *error)
{
[self failureCallbackWithError:error];
}];
Above is the reactivecocoa code I wrote for my login procedure. When the pass and username was correct, the signal would go to sendNext's response block: subscribeNext,
but the issue is, when the credentials were incorrect, and server returned error, neither the sendError nor subscribeError would be triggered.
It's like the error signal was lost or something.
I'm not sure if I am using the right reactivecocoa methods to handle success signal and error signal.
So please help, thanks.
Also please let me know if I'm not clear on my question.
You can use a RACCommand and it will take care of a lot of this for you:
#weakify(self)
RACCommand *submitCommand =
[[RACCommand alloc] initWithEnabled:submitActiveSignal signalBlock:^RACSignal *(id input) {
#strongify(self)
return [[self submitSignal]
doCompleted:^{
#strongify(self)
[self successCallback];
}];
}];
_submitButton.rac_command = submitCommand;
By providing the submitActiveSignal as the initWithEnabled: argument the button will automatically be enabled/disabled based on the value emitted from that signal.
For error handling, RACCommand has a special errors signal that sends every error that occurs in the command. You could subscribe to the errors signal to handle all errors that result from your signal returned from inside the command:
[[submitCommand.errors
takeUntil:self.rac_willDeallocSignal]
subscribeNext:^(NSError *error) {
#strongify(self)
[self failureCallbackWithError:error];
}];
And don't forget to #weakify/#strongify references to self inside of signals to avoid memory leaks.

Is this implementation of a timeout in Reactive Cocoa correct?

I did a login which is connected to a button in ReactiveCocoa. Even I tested this piece of code and it seems to work correctly, I am not sure if I do it right.
The login signal returns "next" on success and "error" in any other case. Since I don't want the Button to be unsubscribed on an error I use the catch function.
What I want: I want a timeout to fire after 2 seconds, if the loginSignal is not fired. Is this correctly done? Is it also done right the "reactive way"?
[[[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
[self disableUI];
}]
flattenMap:^(id value) {
return [[[[[self.login loginSignalWithUsername:self.usernameTextField.text
andPassword:self.passwordTextField.text]
catch:^RACSignal *(NSError *error) {
[self enableUI];
[self showAlertWithTitle:NSLocalizedString(#"ERROR_TITLE", #"Error")
message:NSLocalizedString(#"LOGIN_FAILURE", #"Login not successful.")];
return [RACSignal empty];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
timeout:2.0 onScheduler:[RACScheduler mainThreadScheduler]]
catch:^RACSignal *(NSError *error) {
[self enableUI];
[self showAlertWithTitle:NSLocalizedString(#"TIMEOUT_TITLE", #"Timeout occured")
message:NSLocalizedString(#"REQUEST_NOT_POSSIBLE", #"Server request failed")];
return [RACSignal empty];
}];
}]
subscribeNext:^(id x) {
[self enableUI];
// Go to next page after login
}];
You should in my opinion use RACCommand which is a nice hub for binding signals to UI :
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
return [[[self.login loginSignalWithUsername:self.username password:self.password]
doCompleted:^{
// move to next screen
}]
timeout:2.0 onScheduler:[RACScheduler mainThreadScheduler]];
}];
self.button.rac_command = command;
You can then handle any errors (login or timeout) using the command "errors" signal :
[[command errors] subscribeNext:^(NSError* err) {
// display error "err" to the user
}];
The signal will automatically disable the button while it is executing. If you need to disable other parts of your UI you can use the "executing" signal of the command.
[[command executing] subscribeNext:^(NSNumber* executing) {
if([executing boolValue]) {
[self disableUI];
} else {
[self enableUI];
}
}];
// bonus note: if your enableUI method took a BOOL you could lift it in one line :
[self rac_liftSelector:#selector(enableUI:) withSignals:[command executing], nil];
here is a blog article talking about RACCommands

What is the best way to combine RACCommands into a common result?

Using ReactiveCocoa 2.0, is there a better way to do the following, without having to materialize/dematerialize and still being able to capture errors from any of the 3 signals, without duplicating code?
There are 3 login buttons. Each returns a signal corresponding to an asynchronous "login" API call. Once those finish, they return user objects, errors, and/or completion.
// Login signals
_loginButton.rac_command = [[RACCommand alloc] initWithEnabled:loginValid signalBlock:^RACSignal *(id input) {
return [[API doLogin:_usernameField.text password:_passwordField.text] materialize];
}];
_fbLoginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[API doFacebookLogin] materialize];
}];
_twLoginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[API doTwitterLogin] materialize];
}];
// Login response from any of the 3 signals
[[RACSignal
merge:#[_loginButton.rac_command.executionSignals,
_fbLoginButton.rac_command.executionSignals,
_twLoginButton.rac_command.executionSignals]]
subscribeNext:^(RACSignal *loginSignal) {
RACSignal * s = [loginSignal dematerialize];
[s subscribeNext:^(User *x) {
NSLog(#"user: %#", x);
} error:^(NSError *error) {
NSLog(#"error: %#", error);
} completed:^{
NSLog(#"Completed.");
}];
}];
Since errors are automatically diverted to the errors signal, you normally don't have to deal with materialization or any of that yourself. In fact, that (potential) complexity was the original motivation for the special behavior of errors.
Just merge the error signals and deal with them in one place:
[[RACSignal
merge:#[
_loginButton.rac_command.errors,
_fbLoginButton.rac_command.errors,
_twLoginButton.rac_command.errors,
]]
subscribeNext:^(NSError *error) {
NSLog(#"error: %#", error);
}];
As a sidenote, you can also use -flatten — instead of an inner subscription — to simplify the handling of login responses:
[[[RACSignal
merge:#[
_loginButton.rac_command.executionSignals,
_fbLoginButton.rac_command.executionSignals,
_twLoginButton.rac_command.executionSignals,
]]
// Flattens the signal of `User` signals by one level. The result is
// one signal of `User`s.
//
// This avoids any need for an inner subscription.
flatten]
subscribeNext:^(User *x) {
// This means that a login request completed as well, so there's no need
// for a separate `completed` block.
NSLog(#"user: %#", x);
}];

Resources