Question

I have a demo application to show a possible race condition in the app I am working on. The app displays 2 UITextField: textField1 and textField2.

The following conditions applies:

  • The textField1 string should be at least as long as the textField2 string.
  • The textField2 string should not be longer than 5 characters.

The app uses MVVM pattern and Reactive Cocoa.

The app should save only valid configurations of textField1 and textField2.

Entity.h

@interface Entity : NSManagedObject

@property (nonatomic, retain) NSString * name1;
@property (nonatomic, retain) NSString * name2;

@end

ViewController.h

@interface ViewController : UIViewController

@end

ViewController.m

#import "ViewController.h"
#import "ViewModel.h"

@interface ViewController ()

@property(nonatomic, weak) IBOutlet UITextField *textField1;
@property(nonatomic, weak) IBOutlet UITextField *textField2;

@property(nonatomic, strong) ViewModel *viewModel;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.viewModel = [[ViewModel alloc] init];

    // Two-way binding between  textfield1 and self.viewModel.name1
    // We cannot use a simple RACChannelTo = RACChannelTo because Textfield.text doesn't fire KVO notification on each strokes but only when exit the textfield.
    RACChannelTerminal *name1ModelTerminal = RACChannelTo(self, viewModel.name1);
    RAC(self.textField1, text) = name1ModelTerminal;
    [self.textField1.rac_textSignal subscribe:name1ModelTerminal];

    // Two-way binding between  textfield2 and self.viewModel.name2
    // We cannot use a simple RACChannelTo = RACChannelTo because Textfield.text doesn't fire KVO notification on each strokes but only when exit the textfield.
    RACChannelTerminal *name2ModelTerminal = RACChannelTo(self, viewModel.name2);
    RAC(self.textField2, text) = name2ModelTerminal;
    [self.textField2.rac_textSignal subscribe:name2ModelTerminal];

    // Validation
    RAC(self.textField1, backgroundColor) = [self.viewModel.name1IsValidSignal map:^id(NSNumber *value) {
        if ([value boolValue]) {
            return [UIColor clearColor];
        } else {
            return [UIColor redColor];
        }
    }];

    RAC(self.textField2, backgroundColor) = [self.viewModel.name2IsValidSignal map:^id(NSNumber *value) {
        if ([value boolValue]) {
            return [UIColor clearColor];
        } else {
            return [UIColor redColor];
        }
    }];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

ViewModel.h

#import "RVMViewModel.h"

@interface ViewModel : RVMViewModel

@property(nonatomic, strong) NSString *name1;
@property(nonatomic, strong) NSString *name2;

@property(nonatomic, strong) RACSignal *name1IsValidSignal;
@property(nonatomic, strong) RACSignal *name2IsValidSignal;

@property(nonatomic, strong) RACSignal *isValidSignal;

@end

ViewModel.m

#import "ViewModel.h"
#import "Entity.h"

@interface ViewModel ()

@property(nonatomic, strong) Entity *model;
@property(nonatomic, strong) NSManagedObjectContext *managedObjectContext;

@end

@implementation ViewModel

-(instancetype)init {
    self = [super init];

    if (self) {
        [self initialize];
    }

    return self;
}

-(void)initialize {

    // setup Model
    self.managedObjectContext = [NSManagedObjectContext MR_defaultContext];

    self.model = [Entity MR_createInContext:self.managedObjectContext];

    // log
    [RACObserve(self, name1) subscribeNext:^(NSString *name) {
        NSLog(@"viewModel.name1 = %@", name);
    }];

    [RACObserve(self, name2) subscribeNext:^(NSString *name) {
        NSLog(@"viewModel.name2 = %@", name);
    }];

    [RACObserve(self, model.name1) subscribeNext:^(NSString *name) {
        NSLog(@"model.name1 = %@", name);
    }];

    [RACObserve(self, model.name2) subscribeNext:^(NSString *name) {
        NSLog(@"model.name2 = %@", name);
    }];

    // Validation
    @weakify(self);
    self.name1IsValidSignal = [RACSignal combineLatest:@[RACObserve(self, name1),
                                                         RACObserve(self, name2)]
                                                reduce:^id(NSString *name1, NSString *name2) {
                                                    return @([name1 length] >= [name2 length]);
    }];

    self.name2IsValidSignal = [RACObserve(self, name2) map:^id(NSString *value) {
        return @([value length] < 6);
    }];

    self.isValidSignal = [[RACSignal combineLatest:@[self.name1IsValidSignal, self.name2IsValidSignal]] and];

    // Initial value of Model -> ViewModel
    self.name1 = self.model.name1;
    self.name2 = self.model.name2;

    // Binding: ViewModel -> Model when name is valid

    RAC(self, model.name1) = [[self.name1IsValidSignal filter:^BOOL(id value) {
        return [value boolValue];
    }] map:^id(id value) {
        @strongify(self);

        return self.name1;
    }];

    RAC(self, model.name2) = [[self.name2IsValidSignal filter:^BOOL(id value) {
        return [value boolValue];
    }] map:^id(id value) {
        @strongify(self);

        return self.name2;
    }];

    // Save with Core Data when both name1IsValidSignal and name2IsValidSignal returns @YES and that there are some unsaved changes in the context
    // There is a possible race condition here when this subscription is called before the bindings from ViewModel -> Model is called.
    [[[RACSignal combineLatest:@[RACObserve(self.managedObjectContext, hasChanges), self.isValidSignal]] and] subscribeNext:^(id x) {

        if ([x boolValue]) {
            // Save with Core Data
            [self.managedObjectContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
                // TODO: error management
                if (success || !error) {
                    NSLog(@"Success");
                    NSLog(@" ");
                } else {
                    NSLog(@"Problem when saving to Core Data");
                }

            }];
        }
    }];
}

@end

From my point of view, it is possible that the save to Core Data in the last subscription in ViewModel.m happens before the model is updated. How do you prevent that to happen?

EDIT What I really want is to push to Core Data model only configurations of (name1 and name2) that pass validation.

Xcode Project https://github.com/guillaume-michel/ReactiveValidation

Was it helpful?

Solution

The race condition may be that name2IsValidSignal can be true before name1IsValidSignal is false. Which means that isValidSignal can be true momentarily before turning false (which would trigger the save). One solution is to combine the validation logic into a single unit:

    self.isValidSignal = [RACSignal combineLatest:@[RACObserve(self, name1),
                                                         RACObserve(self, name2)]
                                                reduce:^id(NSString *name1, NSString *name2) {
                                                    return @([name1 length] >= [name2 length] && [name2 length] < 6);
    }];

That should ensure that isValidSignal is only updated after all your validation logic has been run.

OTHER TIPS

According to the docs for -[NSManagedObjectContext hasChanges], you're not supposed to send messages to the NSManagedObjectContext synchronously with receiving a KVO notification:

If you need to send messages to the context or change any of its managed objects as a result of a change to the value of hasChanges, you must do so after the call stack unwinds (typically using performSelector:withObject:afterDelay: or a similar method).

You could accomplish this in ReactiveCocoa by explicitly delivering to +[RACScheduler mainThreadScheduler]:

[[[[RACSignal combineLatest:@[RACObserve(self.managedObjectContext, hasChanges),
                              self.isValidSignal]]
             and]
             deliverOn:RACScheduler.mainThreadScheduler]
             subscribeNext:^(id x) {
    // ...
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top