在ReactiveCocoa中控制ViewController的状态

时间:2017-08-28 09:03:32

标签: ios objective-c reactive-cocoa

我正在实现简单的PincodeViewController。看起来像:

(只是来自谷歌的随机图片,但我的相同)

它有3个步骤:输入当前密码 - >输入新的密码 - >确认。

目前,我创建了3个元素信号,比如

RACSignal *enter4digit = ... // input 4 digits
RACSignal *passcodeCorrect = ... // compare with stored pincode
RACSignal *pincodeEqual = ... // confirm pincode in step 3

将它们绑在一起

RACSignal *step1 = [RACSignal combineLatest:@[enter4digit, passcodeCorrect]];
RACSignal *step2 = [RACSignal combineLatest:@[enter4digit, stage1]];
RACSignal *step3 = [RACSignal combineLatest:@[enter4digit, pincodeEqual, stage2]];

它不起作用。我该如何处理?

任何建议都将不胜感激。谢谢。

1 个答案:

答案 0 :(得分:1)

这里的问题是你有一个输入信号(4位输入),并根据之前发送的信号(以及初始电流密码),会发生不同的事情。

复杂的解决方案

您可以通过在开始时将其分解为不同的信号来解决此问题,在这种情况下,您需要在pincodeInput信号上选择第一个,第二个和第三个值,并对这些信号执行不同的操作,例如:要创建pincodeCorrect信号,您可以:

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]);
}];

其中1234是当前的密码。

对于pincodeEqual,您需要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]);
}];

将它们绑定在一起会让事情变得复杂一些。可以使用if:then:else运算符完成此操作。 我正在为新的pincode创建另一个中间信号,以使代码更具可读性。

如果pincodeEqualYES(当enter4digit上的第二个和第三个值相等时就是这种情况)那么我们返回值,ELSE我们返回一个立即发送的信号一个错误。在此,replyLastsecondInput上的thirdInput非常重要,因为在发送事件后需要多次这个值!

NSError *confirmationError = [NSError errorWithDomain:@"" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Confirmation Wrong"}];
RACSignal *changePincode = [RACSignal
                            if:pincodeEqual
                            then:thirdInput
                            else:[RACSignal error:confirmationError]];

为了获得实际的整个过程,我们再次做同样的事情:

NSError *pincodeError = [NSError errorWithDomain:@"" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Pincode Wrong"}];
RACSignal *newPincode = [RACSignal
                         if:pincodeCorrect
                         then:changePincode
                         else:[RACSignal error:pincodeError]];

信号newPincode现在将在整个过程成功后发送新密码的值,或者错误(当前密码错误或确认错误)

使用状态机

总而言之,我认为上面的解决方案非常复杂,很容易出错,很难将其余的UI连接到流程中。

通过将问题建模为状态机,可以大大简化(在我看来)。

FSM

所以,你将拥有一个状态,并根据该状态和输入,状态发生变化。

typedef NS_ENUM(NSInteger, MCViewControllerState) {
    MCViewControllerStateInitial,
    MCViewControllerStateCurrentPincodeCorrect,
    MCViewControllerStateCurrentPincodeWrong,
    MCViewControllerStateNewPincodeEntered,
    MCViewControllerStateNewPincodeConfirmed,
    MCViewControllerStateNewPincodeWrong
};

实际上,状态会根据当前状态,当前输入以前的输入进行更改。所以让我们为所有这些创建信号:

RACSignal *state = RACObserve(self, state);
RACSignal *input = enter4digit;
RACSignal *previousInput = [input startWith:nil];

我们稍后会zipinputnil一起开始,previousInput总是正好在input之后的1个值,因此当一个新的值已到达input上一个值将同时在previousInput上发送。

现在,我们需要将这三者结合起来创建新状态:

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);
}];

使用zip,我们将为map上的每个新值计算input完全一次的新状态。 在map内,weg总是得到当前状态,当前输入和前一个输入,现在可以根据这三个值计算下一个状态。

通过再次观察self.state并相应地更新您的用户界面,现在可以更轻松地更新您的用户界面,例如显示错误消息或提供restart重新开始(主要是通过将状态机重置为初始状态)。特别是后者在第一个解决方案中要难得多,因为在那里,我们在输入信号上跳过明确的特定数字(然后甚至终止)......