ReactiveCocoa, is there a better way to RAC first and refresh later - ios

Here's the situation
I've a ViewModel (ok, follow the MVVM architecture), which has an array property, called "specialtySegments", and a flag property called "generic", I wanna when the generic value is changed, update the specialtySegments value again
Code snippet
- (instancetype)initWithTerritory:(SAPTerritory *)territory {
self = [super init];
if (self) {
_territory = territory;
_generic = NO;
_selectedProduct = [_products firstObject];
RAC(self, specialtySegments) = [self querySpecialtySegments];
}
return self;
}
- (RACSignal *)querySpecialtySegments {
return [[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self asynQuerySpecialtySegments:^(NSArray *model) {
[subscriber sendNext:model];
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
}];
}] deliverOn:[RACScheduler mainThreadScheduler]] publish] autoconnect];
}
- (void)refreshData {
[self asynQuerySpecialtySegments:^(NSArray *model) {
dispatch_async(dispatch_get_main_queue(), ^{
self.specialtySegments = model;
});
}];
}
And in the ViewController
#weakify(self);
[[RACObserve(self.viewModel, specialtySegments) filter:^BOOL(id value) {
return (value != nil);
}] subscribeNext:^(id x) {
#strongify(self) {
[self.indicatorView stopAnimating];
self.indicatorView.hidden = YES;
[self.tableView reloadData];
}
}];
And some place to change the generic value, like
if (specialtyButton.selected == NO) {
self.viewModel.generic = NO;
genericButton.selected = NO;
specialtyButton.selected = YES;
[self.viewModel refreshData];
}
That's what I know to do now, it's not kind of fancy, I think.
Could I observe the generic property in the ViewModel, and subscripNext and first the signal again.
I've try like this, but code didn't enter the querySpecialtySegments, I put a breakpoint in the method.
[RACObserve(self, generic) subscribeNext:^(id x) {
[self querySpecialtySegments];
}];
and if I do like this, the asynQuerySpecialtySegments method would enter twice, as the RAC before, would run it once, I think
[RACObserve(self, generic) subscribeNext:^(id x) {
[self asynQuerySpecialtySegments:^(NSArray *model) {
dispatch_async(dispatch_get_main_queue(), ^{
self.specialtySegments = model;
});
}];
}];
I'm a newbie to ReactiveCocoa, trying using in the project, so is there a better way to the thing I described above, thanks guy

All correct, it's not works on start, because you are setting instance variable with underscore, if you in init method will call self.generic = NO; , RACObserve will works just as planned.
This happens, because when you setting instance variable, setter will not be called, and methods for KVO like - willChangeValue: and -didChangeValue: also will not be called.

OK, after one day reading I think I've found the answer by myself
The key point is
Most signals start out "cold," which means that they will not do any work until subscription.
So this part code should change like this
[RACObserve(self, generic) subscribeNext:^(id x) {
[[self querySpecialtySegments] subscribeNext:^(id x) { // subscription is the key to trigger
self.specialtySegments = x; // would trigger the observer in view controller to do something
}];
}];
Updated:
Personal idea is to observe generic property in ViewModel and then trigger View to update something is not a best option, some kind of black magic, without calling refresh or something, but the view refresh automatically. With a public method -reloadData is a good choice
- (void)refreshData {
#weakify(self);
[[self querySpecialtySegments] subscribeNext:^(id x) {
#strongify(self) {
self.specialtySegments = x;
}
}];
}

Related

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.

ReactiveCocoa, RACCommand show message or execute segue after completed

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

How to chain signals in a proper way with Reactive Cocoa?

I'm using ReactiveCocoa in a new iOS app. I'm new to reactive programming so I'm still trying to understand what's the proper way to chain signals.
Right now I have the following flow for the "login with Twitter" button.
The ALTUserManager class has the following method for managing the whole login phase by calling some functions in a library that presents the Twitter login panel and does all of the OAuth stuff:
- (RACSignal *)loginTwitter:(UIViewController *)vc {
RACSignal *loginSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[ALTTwitter sharedInstance]isLoggedIn:^(BOOL loggedIn) {
if(loggedIn){
[subscriber sendCompleted];
}
else{
[[ALTTwitter sharedInstance]login:vc andSuccess:^{
[subscriber sendCompleted];
} failure:^(NSString *error) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = error;
[subscriber sendError:[NSError errorWithDomain:#"" code:1 userInfo:userInfo]];
}];
}
}];
return nil;
}];
return loginSignal;
}
I'm using the MVVM pattern so in my ViewModel I've added the following command inside its init method:
self.twitterLoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[ALTUserManager sharedInstance] loginTwitter:nil];
}];
In my view controller I'm handling the presentation logic where I block the interface while showing a progress hud and eventually report the error or go past the login screen if everything is fine:
self.twBtn.rac_command = self.viewModel.twitterLoginCommand;
[self.viewModel.twitterLoginCommand.executionSignals subscribeNext:^(id x) {
NSLog(#"%#", x);
[x subscribeCompleted:^{
NSLog(#"%#", #"completed");
[ALTAlert wait:#""];
[[self.viewModel appLoginWithTwitter] subscribeNext:^(id x) {
NSLog(#"%#", x);
} error:^(NSError *error) {
[ALTAlert dismiss];
[ALTAlert error:error.localizedDescription];
} completed:^{
[ALTAlert dismiss];
#strongify(self);
[self goToChart];
}];
}];
}];
[self.viewModel.twitterLoginCommand.errors subscribeNext:^(NSError *error) {
NSLog(#"Login error: %#", error);
[ALTAlert dismiss];
[ALTAlert error:error.localizedDescription];
}];
I'm pretty sure this could be rewritten in a better way. My concern is mainly about that [x subscribeCompleted] line. What would be the correct approach?
Thanks!
UPDATE
I tried moving all the logic to the ViewModel inside the RACCommand but I still need to catch the errors happening inside the RACCommand.
Subscribing to the errors signal isn't an option as the RACCommand would still return the completed event as well thus making my presentation logic unable to tell if everything went fine or not.
I haven't tried setting a BOOL inside the RACCommand with a side-effect in case of errors and observe it in the view. But that approach seems a bit hacky anyway.
You can simplify the nesting a bit by using the then helper, which will simplify error handling and prevent the separate twitterLoginCommand.errors subscription:
[self.viewModel.twitterLoginCommand.executionSignals subscribeNext:^(id x) {
[x then:^{
NSLog(#"%#", #"completed");
[ALTAlert wait:#""];
return [self.viewModel appLoginWithTwitter];
}] subscribeNext:^(id x) {
NSLog(#"%#", x);
} error:^(NSError *error) {
[ALTAlert dismiss];
[ALTAlert error:error.localizedDescription];
} completed:^{
[ALTAlert dismiss];
#strongify(self);
[self goToChart];
}];
}];
This is a little weird, though. Because you can get into weird states if twitterLoginCommand fires again before the appLoginWithTwitter signal completes. This might not be possible given the rest of the app, but just looking at this block of code in isolation it's something that would concern me.
A better thing to do might be to move that then block into the RACCommand, to ensure that that will never happen (as an RACCommand won't execute again until the previous one finished executing.) Though without seeing more of the code I can't really say if that's a reasonable change.
This is a tricky thing to clean up further because it's inherently side-effectful. If you create a reactive bridge for the ALTAlert class, you could clean up a lot of those subscriptions, as you could just say "look at this signal of signals, and make your state reflect it." Then you can just pass that the execution signals and not have to worry about doing something grosser here.
Then your only real side effect is goToChart, which you can do as something a little simpler:
[[[self.viewModel.twitterLoginCommand.executionSignals flattenMap:^(id x) {
return [x materialize];
}] filter:^(RACEvent *event) {
return event.eventType == RACEventTypeCompleted;
}] subscribeNext:^(id x) {
#strongify(self);
[self goToChart];
}];
Not sure if you have seen the design guidelines, but these show you some solutions on how to avoid the -subscribeNext:error:completed: pattern. Specifically these:
The RAC() or RACChannelTo() macros can be used to bind a signal to a property, instead of performing manual updates when changes occur.
The -rac_liftSelector:withSignals: method can be used to automatically invoke a selector when one or more signals fire.
Operators like -takeUntil: can be used to automatically dispose of a subscription when an event occurs (like a "Cancel" button being pressed in the UI).

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

Using ReactiveCocoa to Respond to User Input

I'm trying to use ReactiveCocoa to control binding and validation on a text field in my app. When I subscribe to a signal, it immediately does the binding from the text field to the model and runs the validation. Normally that wouldn't be an issue, but in this case the field is a 'password' input where the initial value from the model does not get copied to the text field. I want the binding and validation to trigger ONLY when the user actually types something in the field. Does anyone know of a way to do this?
Here is what I'm doing currently:
- (void)setUpBindings: forModel:(NSObject<ValidationModel> *)model {
NSString *property = #"password"
NSInteger throttleTime = 1.5;
[[[self.textField.rac_textSignal distinctUntilChanged]
throttle:throttleTime]
subscribeNext:^(id x) {
NSLog([NSString stringWithFormat:#"Model: %#, Value: %#", [model valueForKey:property], x]);
[model setValue:x forKey:property];
}];
[self bindValidator:[model.validators objectForKey:property]];
}
- (RACSignal *) passwordIsValid {
#weakify(self);
return [[RACObserve(self,password) distinctUntilChanged]
map:^id (NSString *newPassword) {
#strongify(self);
NSArray *errors = [self validatePassword];
return errors;
}];
}
-(void)bindValidator:(RACSignal *)validator
{
if(validator != nil)
{
[[[validator doNext:^(NSArray *errors) {
if(errors.count > 0)
{
NSError *error = [errors firstObject];
self.errorString =error.localizedDescription;
}
else
{
self.errorString = #"";
}
}]
map:^id(NSArray *errors) {
return errors.count <=0 ? #(1) : nil;
}] subscribeNext:^(id x) {
self.isValid = !!x;
}];
}
}
I have found a possible solution. ReactiveCocoa has a rac_signalForControlEvents method that lets you manually specify the event to observe. Using that, I was able to define an initial signal for the UIControlEventEditingChanged event of my text field. I then moved the setup for my binding and validation signals inside the subscribeNext, delaying their subscription until a change event is sent from the text field. My setup method from the OP would look like this:
- (void)setUpBindings:(NSString *)property forModel:(NSObject<ValidationModel> *)model {
NSInteger throttleTime = 1.5;
RACSignal *textChangeSignal = [[self.textField.rac_textSignal distinctUntilChanged]
throttle:throttleTime];
//wait to subscribe to the signal until the user actually makes changes to the field.
//the 'take:1' call ensures that the subscription only happens the first time the event
//is observed.
[[[self.textField rac_signalForControlEvents:UIControlEventEditingChanged]
take:1]
subscribeNext:^(id x) {
[self bindValidator:[model.validators objectForKey:self.reuseIdentifier]];
[textChangeSignal subscribeNext:^(id x) {
[model setValue:x forKey:property];
}];
}];
}
IMPORTANT: Notice the 'take:1' method in the outer chain. You MUST include this. Without that call, the outer 'subscribeNext' will run every time the editing event is fired, resulting in multiple subscribers for the same target and event. For more info see: How do I create a ReactiveCocoa subscriber that receives a signal only once, then unsubscribes/releases itself?
I'm going to leave this as open for now. This way works, but I am sure there must be a cleaner way of doing this.
You can use something like this :
#weakify(self);
RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignal map:^id(NSString *text) {
#strongify(self);
return #([self isValidPassword:text]);
}];
- (BOOL)isValidPassword:(NSString *)password
{
return ([password length] > 0);
}
You can change conditions in isValidPassword to anything you want to.

Resources