0

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;
                              }];
    }
}
pbuchheit
  • 1,371
  • 1
  • 20
  • 47

2 Answers2

0

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.

Community
  • 1
  • 1
pbuchheit
  • 1,371
  • 1
  • 20
  • 47
0

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.

Timur Kuchkarov
  • 1,155
  • 7
  • 21