Reputation: 37
I am implementing the simple PincodeViewController. It looks like:
(just a random image from Google, but mine is the same)
It has 3 steps: enter current pincode -> enter new pincode -> confirm.
Currently, I created 3 element signals, like
RACSignal *enter4digit = ... // input 4 digits
RACSignal *passcodeCorrect = ... // compare with stored pincode
RACSignal *pincodeEqual = ... // confirm pincode in step 3
And bind them together
RACSignal *step1 = [RACSignal combineLatest:@[enter4digit, passcodeCorrect]];
RACSignal *step2 = [RACSignal combineLatest:@[enter4digit, stage1]];
RACSignal *step3 = [RACSignal combineLatest:@[enter4digit, pincodeEqual, stage2]];
It doesn't work. How can I deal with it?
Any advice would be appreciated. Thank you.
Upvotes: 0
Views: 141
Reputation: 3357
The problem here is that you have one input signal (the 4 digit input) and based on what what was sent on that signal before (as well as the initial current pincode), different things should happen.
You could approach this by decomposing this into different signals as you started, in that case you would need to pick out the first, second and third value on the pincodeInput
signal and do different things with those, e.g. to create the pincodeCorrect
signal, you would:
RACSignal *firstInput = [enter4digit take:1];
RACSignal *pincodeCorrect = [[[RACSignal return:@(1234)] combineLatestWith:firstInput] map:^NSNumber *(RACTuple *tuple) {
RACTupleUnpack(NSNumber *storedPincode, NSNumber *enteredPincode) = tuple;
return @([storedPincode isEqualToNumber:enteredPincode]);
}];
where 1234 is the current pincode.
And for pincodeEqual
you need the second and third values of enter4digit
:
RACSignal *secondInput = [[[enter4digit skip:1] take:1] replayLast];
RACSignal *thirdInput = [[[enter4digit skip:2] take:1] replayLast];
RACSignal *pincodeEqual = [[secondInput combineLatestWith:thirdInput] map:^NSNumber *(RACTuple *tuple) {
RACTupleUnpack(NSNumber *pincode, NSNumber *pincodeConfirmation) = tuple;
return @([pincode isEqualToNumber:pincodeConfirmation]);
}];
Binding them together gets a little bit more complicated though. It can be done with the if:then:else
operator.
I'm creating another intermediate signal for the new pincode to make the code more readable.
IF the pincodeEqual
is YES
(which is the case when the second and third value on enter4digit
are equal) THEN we return the value, ELSE we return a signal that immediatly sends an error. Here, the replyLast
on secondInput
and thirdInput
are important, because that value is needed several time after the event was sent!
NSError *confirmationError = [NSError errorWithDomain:@"" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Confirmation Wrong"}];
RACSignal *changePincode = [RACSignal
if:pincodeEqual
then:thirdInput
else:[RACSignal error:confirmationError]];
In order to get the actual whole process, we do almost the same again:
NSError *pincodeError = [NSError errorWithDomain:@"" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Pincode Wrong"}];
RACSignal *newPincode = [RACSignal
if:pincodeCorrect
then:changePincode
else:[RACSignal error:pincodeError]];
The signal newPincode
will now EITHER send the value of the new pincode AFTER the whole process was successful, OR an error (either that the current pincode was wrong, or that the confirmation was wrong)
That all being said, I think the solution above is very convoluted, its easy to make mistakes and hard to wire the rest of your UI to the process.
It can be greatly simplified (in my opinion) by modelling the problem as a State Machine.
So, you'll have a state, and based on that state and an input, the state changes.
typedef NS_ENUM(NSInteger, MCViewControllerState) {
MCViewControllerStateInitial,
MCViewControllerStateCurrentPincodeCorrect,
MCViewControllerStateCurrentPincodeWrong,
MCViewControllerStateNewPincodeEntered,
MCViewControllerStateNewPincodeConfirmed,
MCViewControllerStateNewPincodeWrong
};
Well, actually, the states changes base on the current state, the current input and the previous input. So let's create signals for all these:
RACSignal *state = RACObserve(self, state);
RACSignal *input = enter4digit;
RACSignal *previousInput = [input startWith:nil];
We'll later zip
these together, by starting input
with nil
, previousInput
is always exactly 1 value behind input
, thus when a new value arrives on input
, the previous value will be sent on previousInput
at the same time.
Now, we'll need to combine these three together to create the new state:
RAC(self, state) = [[RACSignal zip:@[state, input, previousInput]] map:^NSNumber *(RACTuple * tuple) {
RACTupleUnpack(NSNumber *currentStateNumber, NSNumber *pincodeInput, NSNumber *previousInput) = tuple;
MCViewControllerState currentState = [currentStateNumber integerValue];
// Determine the new state based on the current state and the new input
MCViewControllerState nextState;
switch (currentState) {
case MCViewControllerStateInitial:
if ([pincodeInput isEqualToNumber:storedPincode]) {
nextState = MCViewControllerStateCurrentPincodeCorrect;
} else {
nextState = MCViewControllerStateCurrentPincodeWrong;
}
break;
case MCViewControllerStateCurrentPincodeCorrect:
nextState = MCViewControllerStateNewPincodeEntered;
break;
case MCViewControllerStateNewPincodeEntered:
if ([pincodeInput isEqualToNumber:previousInput]) {
nextState = MCViewControllerStateNewPincodeConfirmed;
} else {
nextState = MCViewControllerStateNewPincodeWrong;
}
break;
default:
nextState = currentState;
}
return @(nextState);
}];
By using zip
, we will calculate a new state with map
exactly once for each new value that arrives on input
.
Inside map
weg always get the current state, the current input and the previous input and can now calculate the next state based on those three values.
Now its easier to update your UI by observing self.state
again and updating your UI accordingly, e.g. displaying an error message or offering a restart
to start over (essentially by resetting the state machine to the initial state). Especially the latter would be much harder to do in the first solution because there, we're skipping explicit specific numbers on the input signal (and then even terminating)...
Upvotes: 1