Using ReactiveCocoa to Respond to User Input - ios

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.

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.

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

How to prevent a signal to send a next event before subsequent asynchronous calls are completed?

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.

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

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

How to conditionally buffer RACSignal values?

I'm working on some code that interacts with a remote API via websockets. My data layer is responsible for establishing and monitoring the websocket connection. It also contains methods that can be used by the application to enqueue websocket messages to be sent. The application code should not be responsible for inspecting the state of the websocket connection, aka fire-and-forget.
Ideally, I'd like to data layer to function as follows:
When the data layer does not have a connection to the websocket endpoint (self.isConnected == NO), messages are buffered internally.
When a connection is becomes available (self.isConnected == YES), buffered messages are immediately sent, and any subsequent messages are sent immediately.
Here's what I've been able to come up with:
#import "RACSignal+Buffering.h"
#implementation RACSignal (Buffering)
- (RACSignal*)bufferWithSignal:(RACSignal*)shouldBuffer
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
NSMutableArray* bufferedValues = [[NSMutableArray alloc] init];
__block BOOL buffering = NO;
void (^bufferHandler)() = ^{
if (!buffering)
{
for (id val in bufferedValues)
{
[subscriber sendNext:val];
}
[bufferedValues removeAllObjects];
}
};
RACDisposable* bufferDisposable = [shouldBuffer subscribeNext:^(NSNumber* shouldBuffer) {
buffering = shouldBuffer.boolValue;
bufferHandler();
}];
if (bufferDisposable)
{
[disposable addDisposable:bufferDisposable];
}
RACDisposable* valueDisposable = [self subscribeNext:^(id x) {
[bufferedValues addObject:x];
bufferHandler();
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
[subscriber sendCompleted];
}];
if (valueDisposable)
{
[disposable addDisposable:valueDisposable];
}
return disposable;
}];
}
#end
Lastly, this is pseudo-code for how it would be used:
#interface APIManager ()
#property (nonatomic) RACSubject* requests;
#end
#implementation WebsocketDataLayer
- (id)init
{
self = [super init];
if (self) {
RACSignal* connectedSignal = RACObserve(self, connected);
self.requests = [[RACSubject alloc] init];
RACSignal* bufferedApiRequests = [self.requests bufferWithSignal:connectedSignal];
[self rac_liftSelector:#selector(sendRequest:) withSignalsFromArray:#[bufferedApiRequests]];
}
return self;
}
- (void)enqueueRequest:(NSString*)request
{
[self.requests sendNext:request];
}
- (void)sendRequest:(NSString*)request
{
DebugLog(#"Making websocket request: %#", request);
}
#end
My question is: Is this the right approach for buffering values? Is there a more idiomatic RAC way of handling this?
Buffering can be thought of as something that applies to individual requests, which leads to a natural implementation using -flattenMap: and RACObserve:
#weakify(self);
RACSignal *bufferedRequests = [self.requests flattenMap:^(NSString *request) {
#strongify(self);
// Waits for self.connected to be YES, or checks that it already is,
// then forwards the request.
return [[[[RACObserve(self, connected)
ignore:#NO]
take:1]
// Replace the property value with our request.
mapReplace:request];
}];
If ordering is important, you can replace -flattenMap: with -map: plus -concat. These implementations avoid the need for any custom operators, and work without manual subscriptions (which are notoriously messy).
You do almost exactly the same as what is implemented in the bufferWithTime: operation and I can't think of any existing operations that would implement it more idiomatically. (Probably this is the reason why bufferWithTime was implemented in this way.) Reviewing your code using that implementation may reveal some faults you didn't think of.
But to be honest, this should not be so hard. There should exist a buffering operation that buffers the output and spews the contents when the trigger signal fires. Probably most buffering can be implemented in terms of this functionality, so having it would add value to the framework.

Resources