Understanding ReactiveCocoa and MVVM in my ReactiveCocoa test project

最后都变了- 提交于 2019-12-06 01:27:34

问题


I've written a very simple ReactiveCocoa test application to try my hand at coding in RAC (rather than just reading about it endlessly). It's on Github, and I wanted to get some specific questions about it answered. I'll link to the code components as I go along.

First, a brief explanation of the application: it's a timer-driven iteration counter that can be paused by the user. (Its purpose is to count how many seconds have elapsed, eliding the ones where the user paused it.) Once a second, a timer increments a variable iff the user hasn't paused the incrementing behaviour.

There are three classes I'm concerned about hearing feedback for:

  • MPSTicker (.m), which performs "accumulate since initialization unless paused" and provides that result on a signal. It has a public BOOL property to control whether or not accumulation is running.
  • MPSViewModel (.m), which provides a ViewModel wrapping from MPSTicker to the view controller. It provides read-only strings which show the number of ticks and show the text for the action which, if taken, "pauses" or "resumes" ticks. It also has a read-write BOOL for pausing/unpausing ticks.
  • MPSViewController (.m), which consumes strings from MPSViewModel by binding a label to the ViewModel's tick string, binding a button's text to the "tick action" string, and mapping a button's press into the ViewModel's paused property.

My questions:

  1. 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?)
  2. 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.
  3. 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

(Notes: I think I shied away from a signal for the BOOL of paused on MPSTicker because I didn't know how to consume it in the ViewModel to derive two strings (one for the current tick count, and one for the action text), nor how to push UI-driven value changes when the user pushes the "pause" or "resume" button. This is my core concern in questions 1 and 3.)

Some screenshots to help you visualize this gorgeous design:

Ticking:

Paused:


回答1:


This is such an awesome write-up!

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?)

Broadly, there's nothing wrong or non-Reactive about using properties. KVO-able properties can be thought of as behaviors in the academic FRP sense: they're signals which have a value at all points in their lifetime. In fact, in Objective-C properties can be even better than signals because they preserve type information that we'd otherwise lose by wrapping it in a RACSignal.

So there's nothing wrong with using KVO-able properties if it's the right tool for the job. Just tilt your head, squint a bit, and they look like signals.

Whether something should be a property or RACSignal is more about the semantics you're trying to capture. Do you need the properties (ha!) of a property, or do you care more about the general idea of a value changing over time?

In the specific case of MPSTicker, I'd argue the transitions of accumulateEnabled are really the thing you care about.

So if MPSTicker had a accumulationEnabledSignal property, we'd do something like:

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

We're combining both the tick and the enabledness, since it's the transitions of both that drive our logic.

(FWIW, RACCommand is similar and uses an enabled signal: 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.

I may be missing your point here, but I think what you've described is fine. This goes back to the above point about the relationship between properties and signals.

With RAC and MVVM, a lot of the code is simply threading data through to other parts of the app, transforming it as needed in its particular context. It's about the flow of data through the app. It's boring—almost mechanical—but that's kinda the point. The less we have to re-invent or handle in an ah hoc way, the better.

FWIW, I'd change the implementation slightly:

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

That way we're more explicitly defining the relationship of tickString to some transformation of ticker (and we can avoid doing the strong/weak self dance).

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

I'm probably just missing it due to tiredness, but what's the indignity you have in mind here?



来源:https://stackoverflow.com/questions/21419864/understanding-reactivecocoa-and-mvvm-in-my-reactivecocoa-test-project

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!