我编写了一个非常简单的 ReactiveCocoa 测试应用程序来尝试在 RAC 中编码(而不是无休止地阅读它)。在 Github 上,我想得到一些关于它的具体问题的答案。我将链接到代码组件。
首先,对该应用程序进行简要说明:它是一个定时器驱动的迭代计数器,可以由用户暂停。 (它的目的是计算已经过去了多少秒,省略了用户暂停它的秒数。)每秒一次,如果用户没有暂停递增行为,计时器递增一个变量。
我关心三个类的听觉反馈:
- MPSTicker ( .m ),它执行“自初始化以来累积,除非暂停”并在信号上提供该结果。它有一个公共(public)的
BOOL
属性来控制是否运行累加。 - MPSViewModel ( .m ),它提供了一个从
MPSTicker
到 View Controller 的 ViewModel 包装。它提供只读字符串,显示滴答数并显示操作文本,如果采取该操作,“暂停”或“恢复”滴答。它还有一个可读写的BOOL
用于暂停/取消暂停报价。 - MPSViewController ( .m ),它通过将标签绑定(bind)到 ViewModel 的 tick 字符串,将按钮的文本绑定(bind)到“tick action”字符串,并将按钮的按下映射到 ViewModel 的 paused 属性来使用
MPSViewModel
中的字符串.
我的问题:
- 我不喜欢
BOOL
property onMPSTicker
用于启用/禁用它的积累,但我不知道如何更被动地做到这一点。 (这也在 ViewModel 和 ViewController 的下游运行:我如何通过所有这三个运行一个字符串来控制代码是否正在运行?) - ViewModel 公开了
tickString
和tickStateString
作为非常传统的属性,但是使用这些属性的 ViewController 会立即将它们映射回 text on a label和 button text使用RACObserve
。这感觉不对,但我不知道如何公开来自 ViewModel 的信号,以便 ViewController 轻松使用这两个属性。 - ViewController 遇到了 indignity在 ViewModel 上翻转
paused
BOOL
时。我认为这是 #1 的另一个下游效应,“这不应该是BOOL
属性”,但我不确定
(注意:我想我回避了 MPSTicker
上 paused
的 BOOL
信号,因为我不知道如何在 ViewModel 中使用它来派生两个字符串(一个用于当前滴答计数,一个用于操作文本),也不是如何在用户按下“暂停”或“恢复”按钮时推送 UI 驱动的值更改。这是我在问题 1 和 3 中的核心关注点。)
一些屏幕截图可以帮助您形象化这个华丽的设计:
滴答作响:
暂停:
最佳答案
这是一篇很棒的文章!
I don't like the BOOL property on MPSTicker for enabling/disabling its accumulation, but I didn't know how to do it more Reactive-ly. (This also runs downstream to the ViewModel and ViewController: how can I run a string through all three of these to control whether or not the ticker is running?)
从广义上讲,使用属性并没有错或非响应式。 KVO-able 属性可以被认为是学术 FRP 意义上的行为:它们是在其生命周期的所有时间点都具有值(value)的信号。事实上,在 Objective-C 中,属性甚至比信号更好,因为它们保留了类型信息,否则我们会通过将其包装在 RACSignal
中而丢失这些信息。
因此,如果它是完成这项工作的正确工具,那么使用支持 KVO 的属性并没有错。只要倾斜你的头,稍微眯一下,它们看起来就像信号。
某物应该是属性还是 RACSignal
更多是关于您试图捕获的语义。您需要一个属性的特性(哈哈!),还是您更关心值随时间变化的一般概念?
在 MPSTicker
的特定情况下,我认为 accumulateEnabled
的转换才是您真正关心的事情。
所以如果 MPSTicker
有一个 accumulationEnabledSignal
属性,我们会做类似的事情:
_accumulateSignal = [[[[RACSignal
combineLatest:@[ _tickSignal, self.accumulationEnabledSignal ]]
filter:^(RACTuple *t) {
NSNumber *enabled = t[1];
return enabled.boolValue;
}]
reduceEach:^(NSNumber *tick, NSNumber *enabled) {
return tick;
}]
scanWithStart:@(0) reduce:^id(NSNumber *previous, id next) {
// On each tick, we add one to the previous value of the accumulate signal.
return @(previous.unsignedIntegerValue + 1);
}];
我们将 tick 和 enabledness 结合起来,因为正是这两者的转换驱动了我们的逻辑。
(FWIW,RACCommand
类似并使用启用信号:https://github.com/ReactiveCocoa/ReactiveCocoa/blob/9503c6ef7f2f327f4db6440ddfbc4ee09b86857f/ReactiveCocoaFramework/ReactiveCocoa/RACCommand.h#L95。)
The ViewModel exposes tickString and tickStateString as very traditional properties, but the ViewController which consumes these immediately maps them back into text on a label and button text with RACObserve. This feels wrong, but I don't know how to expose a signal from the ViewModel that's easy for the ViewController to consume for these two attributes.
我可能没有理解您的意思,但我认为您的描述很好。这又回到了上面关于属性和信号之间关系的观点。
使用 RAC 和 MVVM,很多代码只是将数据线程化到应用程序的其他部分,根据特定上下文的需要对其进行转换。它是关于通过应用程序的数据流。这很无聊——几乎是机械的——但这就是重点。我们必须以特定方式重新发明或处理的越少越好。
FWIW,我会稍微更改一下实现:
RAC(self, tickString) = [[[[_ticker
accumulateSignal]
deliverOn:[RACScheduler mainThreadScheduler]]
// Start with 0.
startWith:@(0)]
map:^(NSNumber *tick) {
// Unpack the value and format our string for the UI.
NSUInteger count = tick.unsignedIntegerValue;
return [NSString stringWithFormat:@"%i tick%@ since launch", count, (count != 1 ? @"s" : @"")];
}];
这样我们就可以更明确地定义 tickString
与 ticker
的某种转换之间的关系(并且我们可以避免执行强/弱 self
跳舞)。
The ViewController suffers an indignity when flipping the paused BOOL on the ViewModel. I think this is another downstream effect of #1, "This shouldn't be a BOOL property", but I'm not sure
我可能只是因为疲倦而想念它,但你在这里想到的侮辱是什么?
关于ios - 在我的 ReactiveCocoa 测试项目中了解 ReactiveCocoa 和 MVVM,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21419864/