Posible estado de carrera con ReactiveCocoa en la aplicación MVVM con datos básicos

Tengo una aplicación de demostración para mostrar una posible condición de carrera en la aplicación en la que estoy trabajando. La aplicación muestra 2 UITextField: textField1 y textField2.

Se aplican las siguientes condiciones:

  • La cadena textField1 debe ser al less tan larga como la cadena textField2.
  • La cadena textField2 no debe tener más de 5 caracteres.

La aplicación usa el patrón MVVM y el Cocoa Reactivo.

La aplicación debe save solo configuraciones válidas de textField1 y textField2.

Entidad.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 networkingColor]; } }]; RAC(self.textField2, backgroundColor) = [self.viewModel.name2IsValidSignal map:^id(NSNumber *value) { if ([value boolValue]) { return [UIColor clearColor]; } else { return [UIColor networkingColor]; } }]; } - (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)] networkinguce:^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 

Desde mi punto de vista, es posible que la guardada en Core Data en la última suscripción en ViewModel.m ocurra antes de que se actualice el model. ¿Cómo evitas que eso ocurra?

EDITAR Lo que realmente quiero es empujar al model de Core Data solo las configuraciones de (nombre1 y nombre2) que pasan la validation.

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

La condición de carrera puede ser que name2IsValidSignal puede ser verdadero antes de name1IsValidSignal es falso. Lo que significa que es ValidSignal puede ser cierto momentáneamente antes de convertir falso (lo que dispararía el guardado). Una solución es combinar la lógica de validation en una sola unidad:

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

Eso debería garantizar que isValidSignal solo se actualiza después de que se haya ejecutado toda su lógica de validation.

De acuerdo con los documentos para -[NSManagedObjectContext hasChanges] , se supone que no debe enviar posts al NSManagedObjectContext de forma síncrona con la recepción de una notificación de KVO:

Si necesita enviar posts al context o cambiar cualquiera de sus objects administrados como resultado de un cambio en el valor de hasChanges, debe hacerlo después de que la stack de llamadas se desenrolle (normalmente se utiliza performSelector: withObject: afterDelay: o un método similar )

Podría lograr esto en ReactiveCocoa al entregar explícitamente a +[RACScheduler mainThreadScheduler] :

 [[[[RACSignal combineLatest:@[RACObserve(self.managedObjectContext, hasChanges), self.isValidSignal]] and] deliverOn:RACScheduler.mainThreadScheduler] subscribeNext:^(id x) { // ...