I'm using ReactiveCocoa and Overcoat/Mantle/AFNetworking to fetch data and authenticate a user from a RESTful API.
This is the code in the login view controller that manages the login button and textfields for credentials:
#weakify(self);
self.loginButton.rac_command =
[[RACCommand alloc] initWithEnabled:validCredentials
signalBlock:^RACSignal *(id input) {
#strongify(self);
return [[PFUserManager sharedManager] logInUser:self.usernameTextField.text
password:self.passwordTextField.text];
}];
// Handle errors for the login command
[self.loginButton.rac_command.errors subscribeNext:^(NSError *error) {
// Present the error message
[PFErrorAlertFactory showOVCError:error];
}];
// Take care of the signal from the request
[[self.loginButton.rac_command.executionSignals flatten] subscribeNext:^(NSNumber *success) {
#strongify(self);
[self clearTextFields];
[self.flowController controllerForMainScreen]; // Transition to "logged in state"
} error:^(NSError *error) {
#strongify(self);
[self clearTextFields];
}];
I have this method on a singleton UserManager class:
- (RACSignal *)logInUser:(NSString *)username password:(NSString *)password {
// Return a cold signal that sends next and complete when user is authenticated and error if authentication failed.
PFAPIClient *client = [[PFAPIClient alloc] initWithUsername:username password:password];
#weakify(self);
RACSignal *loginSignal = [[client rac_POST:kAuthenticationResourcePath parameters:nil] doNext:^(OVCResponse *response) {
#strongify(self);
self.currentUser = response.result;
NSError *error;
[SSKeychain setPassword:password forService:kKeychainServiceKey account:self.currentUser.username error:&error];
if (error) {
[PFErrorAlertFactory showLocalizedDescriptionOfError:error];
}
}];
return loginSignal;
}
This is all good, using this signal as a RACCommand signal for the button. I handle next, error and completed events in the login view controller and it works fine.
As you see in the UserManager code, in the doNext block, I show an error if the Keychain method returns one. I'm a little uncertain as to if this error handling belongs in this class.
It does work, the error shows as an UIAlertView, but should this UserManager class really be responsible for showing the error?
Errors coming from the rac_POST signal is handled by the login view controller, and I would like to handle the error from the Keychain method here as well. Is it possible to send an error to the subscriber of the rac_POST signal from within the doNext block? I'm missing a pointer to the subscriber though... As well, if an error occurs in the Keychain method, the signal still sends next and complete and the login is a success as far as the calling view controller knows. This is clearly not the way it's supposed to work.
Is there any other preferred way of handling this whole situation? I know that side effects in doNext blocks aren't preferred, but in this case I see no other solution as I want the UserManager to own this method and be able to set its own currentUser. Should I wrap this in a new signal and explicitly send next, complete and error instead?
Regards,
Jens
You can create SSKeychain category:
#interface SSKeychain (RACExtension)
- (RACSignal*)rac_setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
#end
#implementation SSKeychain (RACExtension)
- (RACSignal*)rac_setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account
{
[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSError *error;
BOOL result = [SSKeychain setPassword:password forService:service account:account error:&error];
if (result) {
[subscriber sendNext:#(result)];
[subscriber sendCompleted];
} else {
[subscriber sendError:error];
}
}];
}
#end
And then use it your UserManager:
- (RACSignal *)logInUser:(NSString *)username password:(NSString *)password
{
PFAPIClient *client = [[PFAPIClient alloc] initWithUsername:username password:password];
#weakify(self);
return [[[client rac_POST:kAuthenticationResourcePath parameters:nil]
flattenMap:^RACStream *(OVCResponse *response){
#strongify(self)
return [[SSKeychain rac_setPassword:password forService:kKeychainServiceKey account:self.currentUser.username]
mapReplace:response];
}]
doNext:^(OVCResponse *response){
#strongify(self)
self.currentUser = response.result;
}]
}
Related
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
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.
I'm getting stuck with an error when using my Watchkit Application. When I launch it, I ask the containing iOS app to get some data from network. The problem is that I get an error saying the containing app never calls 'reply()' :o But looking at my code, it should call it.
I tried to debug every step from openParentApplication to the 'reply()' call, and it seems to work well =X
Here is my code in the Watchkit extension
- (void)initDiaporamasWithSuccess:(void (^)())success andFailure:(void (^)(NSError*))failure {
NSLog(#"Ask to load diapos");
__weak typeof(self) weakSelf = self;
[WKInterfaceController openParentApplication:#{#"watchKit": #"watchKit.initDiapos"} reply:^(NSDictionary *replyInfo, NSError *error) {
if (error) {
NSLog(#"%#", error);
if (failure) {
failure(error);
}
return;
}
NSLog(#"got items : %#", replyInfo[#"diapos"]);
weakSelf.diaporamas = replyInfo[#"diapos"];
[weakSelf setDiaporama:replyInfo[#"firstDiapo"] AtIndex:0];
if (success) {
success();
}
}];
}
The result should be an NSDictionary containing an NSArray with some diaporamas basic informations, and an object (Diapo) containing the full informations of the first diaporama (e.g. self.diaporamas[0])
And here is the code in the containing app's AppDelegate :
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
// Maybe we could handle multiple watchKit extension calls that way ?
// Something like a key-value 'protocol' to run the right block of code
NSString *watchKitCall = userInfo[#"watchKit"];
NSLog(#"watchKit handled");
if ([watchKitCall isEqualToString:#"watchKit.initDiapos"]) {
[AppDelegate watchInitialObjects:^(NSDictionary *info) {
NSLog(#"Managed to get initial infos");
reply(info);
} failure:^(NSError *error) {
NSLog(#"Fail : %#", error);
reply(#{#"error": error});
}];
}
}
+ (void) watchInitialObjects:(void (^)(NSDictionary *info))success failure:(void (^)(NSError *error))failure {
NSDictionary *parameters = #{#"site" : #(14), #"limit" : #(10)};
[AppDelegate requestDiapoListWithParams:parameters success:^(NSArray *items) {
if ([items count] == 0)
{
NSError *error = [NSError errorWithDomain:#"com.domain.app" code:404 userInfo:nil];
failure(error);
return;
}
Diapo *firstDiapo = [items firstObject];
[AppDelegate requestDiapoDetailWithDiapo:firstDiapo success:^(Diapo *diapo) {
if (!diapo)
{
NSError *error = [NSError errorWithDomain:#"com.domain.app" code:404 userInfo:nil];
failure(error);
return;
}
NSDictionary *result = #{
#"firstDiapo" : diapo,
#"diapos" : items
};
success(result);
} failure:^(NSError *error) {
failure(error);
}];
} failure:^(NSError *error) {
failure(error);
}];
}
In the watchKitHandler, I call watchInitialObjects to get the diaporamas array and the first diaporama's informations.
In the watchInitialObjects, I make a first network call to get the array, and on success, I make an other network call to get the firs diaporama informations.
To make the calls and map the JSON into objects, I use RESTKit
I really don't get what could be the error =x
UPDATE
I forgot to write the error I get, here it is :
Error Domain=com.apple.watchkit.errors Code=2 "The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]" UserInfo=0x7fcb53e12830 {NSLocalizedDescription=The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]}
And I kept trying to know why I get this error, and I think I found it:
It seems that there is a (very little) timeout to do the work in the containing app. But I mapped the JSON data I received directly in the containing app and then, send those custom objects in the reply(). But when I removed the mapping part, it worked well !
So...that's why I think that was the problem =X
Does anybody could approve my thoughts or corrects me ?
After hours of searching and testing different codes, I finally found my problem...and it's obvious when we read the Apple documentation about 'application:handleWatchKitExtensionRequest:reply:' seriously...
here is the answer : (it's in the documentation)
The contents of the dictionary must be serializable to a property list file.
Which means that objects can ONLY be dictionaries, arrays, strings, numbers (integer and float), dates, binary data, or Boolean values
...I feel dumb ><
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).