If I assign a signal to a property of a control:
RAC(self.loginButton.enabled) = [RACSignal
combineLatest:@[
self.usernameTextField.rac_textSignal,
self.passwordTextField.rac_textSignal
] reduce:^(NSString* username, NSString* password) {
return @(username.length > 0 && password.length > 0);
}];
But then wanted to assign a different RACSignal
to enabled
, how can I clear any existing one before doing so?
If I try and set it a second time, I get an exception like the following:
2013-10-29 16:54:50.623 myApp[3688:c07] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Signal <RACSignal: 0x975e9e0> name: +combineLatest: (
"<RACSignal: 0x975d600> name: <UITextField: 0x10f2c420> -rac_textSignal",
"<RACSignal: 0x975de30> name: <UITextField: 0x10f306e0> -rac_textSignal"
) reduce: is already bound to key path "self.loginButton.enabled" on object <LoginViewController: 0x10f264e0>, adding signal <RACSignal: 0x9763500> name: +combineLatest: (
"<RACSignal: 0x97624f0> name: <UITextField: 0x10f2c420> -rac_textSignal",
"<RACSignal: 0x97629e0> name: <UITextField: 0x10f306e0> -rac_textSignal"
) reduce: is undefined behavior'
A big part of ReactiveCocoa's philosophy is the elimination of state. State is anything that can change in-place over time, and it's problematic for a few reasons:
- You lose past information. Once a variable has been changed, it's like the previous values never existed.
- Changes can come from any number of places. It's hard to look at stateful code and know exactly what will happen when — there's poor locality.
- Concurrency and asynchrony makes state management difficult, because you now have to coordinate changes across multiple execution points. Determinism is hard to achieve when there are multiple actors that may conflict with each other.
The reason RAC disallows multiple bindings to the same property is that it makes the ordering nondeterministic. If I have two signals bound to enabled
, which one takes precedence? How would I know which one sent the latest value?
The reason RAC disallows rebinding the same property is that it's a stateful thing to do. Changing the binding in-place is imperative, and bad for all the reasons outlined above.
Instead, use signals as a declarative way to express changes over time. Everything that changes a property — now or in the future — should be represented in one signal.
Based on your example, it's hard to know exactly what those inputs would be, but let's say you wanted to use different login text fields based on the value of a UISwitch
:
// A signal that automatically updates with the latest value of
// `self.emailLoginSwitch.on`.
RACSignal *emailLoginEnabled = [[[self.emailLoginSwitch
rac_signalForControlEvents:UIControlEventValueChanged]
mapReplace:self.emailLoginSwitch]
map:^(UISwitch *switch) {
return @(switch.on);
}];
// Whether the user has entered a valid username and password.
RACSignal *usernameAndPasswordValid = [RACSignal
combineLatest:@[
self.usernameTextField.rac_textSignal,
self.passwordTextField.rac_textSignal
] reduce:^(NSString* username, NSString* password) {
return @(username.length > 0 && password.length > 0);
}];
// Whether the user has entered a valid email address.
RACSignal *emailValid = [self.emailTextField.rac_textSignal map:^(NSString *email) {
return @(email.length > 0);
}];
// Uses different conditions for validity depending (ultimately) on the value of
// `self.emailLoginSwitch`.
RAC(self.loginButton, enabled) = [RACSignal
if:emailLoginEnabled
then:emailValid
else:usernameAndPasswordValid];
In this way, the binding remains valid no matter what the inputs actually are (username/password or email), and we've avoided any need to mutate things at runtime.
Instead of using RAC
macro, can you explicitly use :
[[RACSignal
combineLatest:@[
self.usernameTextField.rac_textSignal,
self.passwordTextField.rac_textSignal
] reduce:^(NSString* username, NSString* password) {
return @(username.length > 0 && password.length > 0);
}] setKeyPath:@"enabled" onObject:self.loginButton nilValue:nil];
For version < 2.0, use -toProperty:onObject:
instead
I'm not sure if it handle your case, but try it, I'm new to ReactiveCocoa
, but sure anyone can help you out, just stay tuned :)