【问题标题】:Control the state of ViewController in ReactiveCocoa在 ReactiveCocoa 中控制 ViewController 的状态
【发布时间】:2017-08-28 09:03:32
【问题描述】:

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

(只是一张来自 Google 的随机图片,但我的是一样的)

它有 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]];

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

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

【问题讨论】:

    标签: ios objective-c reactive-cocoa


    【解决方案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 上的第二个和第三个值相等的情况)那么我们返回该值,否则我们返回一个立即发送错误的信号。在这里,secondInputthirdInput 上的 replyLast 很重要,因为在发送事件后多次需要该值!

    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 的其余部分与流程联系起来。

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

    因此,您将拥有一个状态,并且基于该状态和输入,状态会发生变化。

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

    嗯,实际上,状态会根据当前状态、当前输入之前的输入而改变。所以让我们为所有这些创建信号:

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

    我们稍后将zip 这些一起,通过以nil 开头inputpreviousInput 总是正好落后input 1 个值,因此当一个新值到达input 时,previous 值将同时发送到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,我们将为到达input 的每个新值精确一次 计算一个带有map 的新状态。 在map weg 中始终获取当前状态、当前输入和前一个输入,现在可以根据这三个值计算下一个状态。

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

    【讨论】:

    • 两种方式都很棒,尤其是状态机。谢谢师父。我在哪里可以学习 RX 技术,例如状态机?
    • 以这种方式使用状态机的想法来源于redux
    猜你喜欢
    • 1970-01-01
    • 2019-11-25
    • 1970-01-01
    • 2014-05-31
    • 2011-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多