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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With