在 ReactiveCocoa 中控制 ViewController 的状态
Control the state of ViewController in ReactiveCocoa
我正在实现简单的 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]];
没用。我该如何处理?
如有任何建议,我们将不胜感激。谢谢。
这里的问题是你有一个输入信号(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 创建另一个中间信号,以使代码更具可读性。
如果 pincodeEqual
是 YES
(当 enter4digit
上的第二个和第三个值相等时就是这种情况)那么我们 return 这个值,否则我们return 立即发送错误的信号。在这里,secondInput
和 thirdInput
上的 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
将它们放在一起,通过从 input
开始 nil
,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
,我们将为每个到达 input
的新值计算 map
恰好 一次 的新状态。
在 map
内部,weg 始终获取当前状态、当前输入和先前输入,现在可以根据这三个值计算下一个状态。
现在,通过再次观察 self.state
并相应地更新您的 UI,可以更轻松地更新您的 UI,例如显示错误消息或提供 restart
以重新开始(主要是通过将状态机重置为初始状态)。尤其是后者在第一个解决方案中更难做到,因为在那里,我们跳过了输入信号上的显式特定数字(然后甚至终止)...
我正在实现简单的 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]];
没用。我该如何处理?
如有任何建议,我们将不胜感激。谢谢。
这里的问题是你有一个输入信号(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 创建另一个中间信号,以使代码更具可读性。
如果 pincodeEqual
是 YES
(当 enter4digit
上的第二个和第三个值相等时就是这种情况)那么我们 return 这个值,否则我们return 立即发送错误的信号。在这里,secondInput
和 thirdInput
上的 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
将它们放在一起,通过从 input
开始 nil
,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
,我们将为每个到达 input
的新值计算 map
恰好 一次 的新状态。
在 map
内部,weg 始终获取当前状态、当前输入和先前输入,现在可以根据这三个值计算下一个状态。
现在,通过再次观察 self.state
并相应地更新您的 UI,可以更轻松地更新您的 UI,例如显示错误消息或提供 restart
以重新开始(主要是通过将状态机重置为初始状态)。尤其是后者在第一个解决方案中更难做到,因为在那里,我们跳过了输入信号上的显式特定数字(然后甚至终止)...