Reactor in an Objective-C library that provides mechanisms for writing transparently reactive code. It's based on the Tracker library from Meteor.js, which you can view the source for here.
An example reactive function using Reactor looks something like this:
[MTRReactor autorun:^(MTRComputation *computation) {
self.counterLabel.text = @(self.counter).description;
}];
Which re-runs automagically as the counter property updates:
for(int i=0 ; i<100 ; i++) {
self.counter++;
}
Seems like there's a wizard behind the curtain, right? There is! Let's get a good look at his underthings. Reactor has two core components, MTRComputation
and MTRDependency
, that are linked up on the sly by MTRReactor
to provide reactivity.
An MTRComputation
is created every time you call -autorun:
—it's an object that encapsulates the block you pass to
the same. Only one computation can run at a time, and the running computation is known as the
"current computation"
The block passed to -autorun:
is called immediately to update the view, but it's also used to infer what
this new computation's dependencies are. How does that happen?
Here's the wizard's torso (it's what he uses to wave hello):
- (NSInteger)counter
{
[self.counterDependency depend];
return _counter;
}
This bit was left off the original example. In the accessor for counter
we're secretly calling -depend
another
property, counterDependency
, which we created earlier:
self.counterDependency = [MTRDependency new]
When you call -depend
on a dependency, Reactor looks at the current computation and adds it as a dependent of your
dependency. The legs, the powertrain:
- (void)setCounter:(NSInteger)counter
{
_counter = counter;
[self.counterDependency changed];
}
When -changed
is called on a dependency, all of its dependent computations will be re-run. This updates your UI, re-establishes the dependent relationships, and kicks-off the cycle all over again. It's not that magical, eh? With legs and a torso you're as capable as any wizard.
Creating all those dependencies is pretty tedious. Worse, if you override the setter and getter for a property to trigger a dependency its storage isn't @synthesized
anymore and you have to do it manually.
Enter MTRReactive
. Annotate any of your classes with this protocol, and all its properties become implicitly reactive:
@interface Person : NSObject <MTRReactive>
@property (copy , nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
@end
If you want to whitelist/blacklist certain properties, you can implement either +reactiveProperties:
or +nonreactiveProperties:
, respectively.
There are a few caveats to keep in mind:
- Just because your superclass adopts
MTRReactive
doesn't mean your properties are also reactive. Every class that wants reactivity must adopt the protocol independently. - Properties which don't have a setter won't be reactive, as there's no way to invalidate its dependency.
- You'll need to register classes conforming to
MTRReactive
, and there are two ways of doing that:-
Automatically, sending a message to
[MTRReactor engage]
. You can do that, for example, on- (BOOL)applicationDidFinishLaunching:
. -
On-demand, sending a message to
[MTRReactor reactify:self.class]
. You can override+ (void)initialize
and add that message.
-
You can then react to your objects' properties in computations like normal:
[MTRReactor autorun:^(MTRComputation *computation) {
self.ageLabel = @(person.age).description;
}];