For the last couple months I’ve been doing a lot of ReactiveCocoa for different iOS projects and let me tell you; it is pretty fucking awesome!!!. I know it’s been around for a couple years … yeah yeah Swift is the big thing now, Objective-C is dead and the Swift version of ReactiveCocoa is around the corner (maybe).
I don’t actually think Objective-C is dead and I am probably still going to use it, at least until the libraries get a little more mature and XCode stops crashing 10 times per day.
As you may know
RACCommand is an abstraction that models a command, a user initiated action that may have some side effects. You can create a new
RACCommand object using the
initWithEnabled:signalBlock: initializer method. This method receives a signal as the first parameter and a block that receives an input and returns a signal as the second parameter.
The block will be called every time the
execute: method is invoked on the
RACCommand object. The object that receives the block is the object that is passed to
execute:. The signal returned by the block will be the signal returned by
The signal is used to decide whether the command is enabled or disabled. This is pretty useful because when you do something like
self.button.rac_command = self.viewModel.someCommand the enabled property of the button is automatically changed when the command is enabled or disabled, avoiding all the boilerplate code to keep the button state synced.
Assuming we have the following interfaces
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
a possible implementation for the login command could be
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Based on this implementation, unless the username has 5 characters and the password has 4 characters the login command will not be enabled. Executing a disabled command (calling its
execute: method) will result in a signal that will error with domain
RACCommandErrorDomain and code
Now lets analyze a different example of how to use a
RACCommand. Lets take for instance pagination. Most of the apps nowadays have some kind of newsfeed or activity stream. A very simple implementation of this view could fetch all the required data and display it on a
UITableView. This could work pretty well if the data that needs to be displayed is not really big. But if we are talking about something like the Twitter’s newsfeed doing just one query to the backend service to display all the user’s newsfeed could result in a DOS or at least it would take a lot of time to answer. This is good situation to apply pagination.
We can implement a simple view model that knows how to display a paginated list and later we can bind that view model against a
UITableViewController. We can call that view model
TableViewModel and a naive implementation of that view model could be
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
As you can see the implementation of
TableViewModel is pretty simple, the important part is in the private
performFetch method that is called inside the signal block associated with the
fetchNextPage command. We are using replay before returning the signal in
performFetch to cache the result of the map and
avoid the execution of the side effects (to increase the
nextPage counter) in case several subscriptions get created to this signal.
Now that we have implemented
TableViewModel it’s time to test it and in order to do that I use Specta + Expecta matchers + OCMockito. For the purpose of this blog post I am only going to show a reduced version of the
The following spec asserts that after calling
fetchNextPage the page counter gets increased. In this case we are calling
fetchNextPage 3 times thus making the last value of
requestPage equal to 2 (because we have requested page 0, 1 and 2). We only want to fetch a page after the previous page was successfully fetched. That is why we are using
concat: because it will subscribe to the concatenated signal after the first signal has completed.
completionSignal is a signal that will first fetch page 0 and after it’s completed it will fetch page 1 and after it’s completed it will fetch page 2 and then will complete. If any of the concatenated signals errors
completionSignal, will error immediately.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
Unfortunately if you run the previous spec you will get the following error
failed to invoke done() callback before timeout (10.000000 seconds)
meaning that for some reason the subscribed block never got executed and the only way that that could’ve happened is if one
of the concatenated signals has failed. To verify this theory we can
subscribeError: instead of
When I did this I realized that indeed the signal was sending an error and the error code was
This is super weird because the
fetchNextPage command is enabled/disabled based on the
consumedAllPages property and
the only way this could be set to
NO is if the fetcher’s signal returns an empty array and that is impossible because
we are using a fake fetcher that always returns a non-empty array.
Digging a little bit inside the internals of
RACCommand I realized that
execute: does not actually use the given signal to
decide if the command can be executed or not. (Check this line and also this line). When the
method is invoked, it first gets a value from
immediateEnabled which is a combination of the provided enabled signal
and another signal which is basically based on
the enabled signal sends
YES and if
NO (which is the default) the
executing property must
What is happening and causing the test to fail is that when
execute: gets invoked for the second time the change on
executing signal has not been propagated yet and although the first invocation of
execute: has finished the
internal state of the
RACCommand does not reflect that.
In a real-case scenario this is virtually impossible to happen. At least if the
RACCommand is bound to an event
triggered by the user, because this would probably happen in two different run loops and by that time the change
executing property would be propagated.
Finally the easy fix to make the test pass is to add the following statement in the
tableViewModel.fetchNextPage.allowsConcurrentExecution = YES;
Which I think is a valid trade-off to be made. What do you guys think? Do you have a better solution?