In my application I have a signal which triggers some asynchronous network activity via flattenMap. I want to display a loading indicator while the network activity is in progress.
My current solution works just fine:
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
// show the loading indicator as a side-effect
self.loadingIndicator.hidden = NO;
}]
flattenMap:^id(id x) {
return [self doSomethingAsync];
}]
subscribeNext:^(NSNumber *result) {
// hide the indicator again
self.loadingIndicator.hidden = YES;
// do something with the results
}];
This works, however I would like to change the above code so that the hidden property of the loading indicator can be set via a signal.
Is this possible?
Elsewhere in my app I have more complex requirements where the visibility of an element depends on a few different 'events', being able to compose these via signals would be much better.
RACCommand is tailor-built for exactly this use case, and will usually result in dramatically simpler code than the alternatives:
#weakify(self);
RACCommand *signInCommand = [[RACCommand alloc] initWithSignalBlock:^(id _) {
#strongify(self);
return [self doSomethingAsync];
}];
self.signInButton.rac_command = signInCommand;
// Show the loading indicator while signing in.
RAC(self.loadingIndicator, hidden) = [signInCommand.executing not];
It looks like your signal is: when signInButtonSignal or resultSignal send a value, invert the last value of hidden. That's easy enough.
[[[hiddenSig replayLast] not] sample:[RACSignal merge:#[signInButtonSignal, resultSignal]];
I'm using exactly that construct for a situation similar to yours. It might be nice to wrap it up into an operator:
- (RACSignal *)toggle:(RACSignal *)toggler
{
return [[[self replayLast] not] sample:toggler];
}
Then you have just
[hiddenSig toggle:[RACSignal merge:#[signInButtonSignal, resultSignal]]];
Another possibility might be a class method, tying the state to a mapping Block:
+ (RACSignal *)toggle:(RACSignal *)toggler initially:(BOOL)initial
{
__block BOOL currVal = initial;
return [[toggler map:^id (id _) {
currVal = !currVal;
return #(currVal);
}] startWith:#(initial)];
}
and then
[RACSignal toggle:[RACSignal merge:#[signInButtonSignal, resultSignal]]
initially:NO];
The answer from Josh helped quite a bit, but in the end I found a simpler solution. Simply breaking the pipeline into two signals, one for the button press, the other for the subsequent asynchronous activity. I then merged the two to give a signal which I used to bind to the loadingIndicator's hidden property:
// a signal that triggers sign-in
RACSignal *signInStartSignal = [self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside];
// a signal that emits the sign in result
RACSignal *signInResultSignal =
[signInStartSignal
flattenMap:^id(id x) {
return [self doSomethingAsync];
}];
[signInResultSignal
subscribeNext:^(NSNumber *result) {
// do something based on the result
}];
// merge the two signals
RACSignal *signInInProgress =
[[RACSignal merge:#[signInResultSignal, signInStartSignal]]
map:^id(id value) {
// if the signal value is a UIButton, the signal that
// just fired was the signInStartSignal
return #(![[value class] isSubclassOfClass:[UIButton class]]);
}];
RAC(self.signInFailureText,hidden) = signInInProgress;
Related
I'm trying to disable a UIBarButtonItem in my iOS app when a condition has been met.
So in my viewModel I created this signal:
-(RACSignal *)thresholdLimitReachedSignal
{
#weakify(self);
return [RACObserve(self, thresholdLimitReached) filter:^BOOL(id value) {
#strongify(self);
return self.thresholdLimitReached;
}];
}
Then in my viewController I have this:
self.requestNewPinButton.rac_command = [[RACCommand alloc]initWithEnabled:self.viewModel.thresholdLimitReachedSignal
signalBlock:^RACSignal *(id input) {
[self.viewModel.requestNewPinSignal subscribeNext:^(id x) {
//do some stuff here
}];
return [RACSignal empty];
}];
So the UIBarButtonItem is triggered and fires off a requestNewPinSignal which works just fine. Then I flag thresholdLimitReached which causes the thresholdLimitReachedSignal to fire - all good. However the button just does not get disabled and I am not sure why? No matter if I manually set the boolean to true or false inside the thresholdLimitReachedSignal method - button remains enabled!
If I manually subscribe to thresholdLimitReachedSignal
like so:
[self.viewModel.thresholdLimitReachedSignal subscribeNext:^(id x) {
self.requestNewPinButton.enabled = NO;
}];
Then button gets disabled no problem. I'd like to have this signal combined with the requestSignal some how - I thought initWithEnabled:signalBlock did this?
[RACObserve(self, thresholdLimitReached) filter:^BOOL(id value) {
#strongify(self);
return self.thresholdLimitReached;
}];
You're filtering thresholdLimitReachedSignal so that it only ever returns YES, so your button is always going to be enabled. For starters, you could rewrite that like this and avoid the #weakify/#strongify:
[RACObserve(self, thresholdLimitReached) filter:^BOOL(NSNumber *thresholdLimitReached) {
return thresholdLimitReached.boolValue;
}];
But don't do that: if you're using this as the enabled signal, it needs to be a signal of booleans that sends YES when it should be enabled and NO when it should be disabled.
Assuming that your want the button to be disabled when the threshold has been reached, you want something like this:
[[RACCommand alloc] initWithEnabled:[RACObserve(self.viewModel, thresholdLimitReached) not]
signalBlock:...];
I am trying to change the label on a button up on the selector being called.
It appears that the code is duplicated. Is there a way perhaps it's not obvious to me right now to have the signal switch after the map ? or no ?
[[[pressedStart map:^id(id value) {
UIButton* button = value;
BOOL transform = [button.titleLabel.text isEqualToString:#"Start"];
return [NSNumber numberWithBool:transform];
}] filter:^BOOL(id value) {
return [value boolValue];
}] subscribeNext:^(id x) {
self.start.titleLabel.text = #"Stop";
}];
[[[pressedStart map:^id(id value) {
UIButton* button = value;
BOOL transform = [button.titleLabel.text isEqualToString:#"Stop"];
return [NSNumber numberWithBool:transform];
}] filter:^BOOL(id value) {
return [value boolValue];
}] subscribeNext:^(id x) {
self.start.titleLabel.text = #"Start";
}];
First of all, in order to change the button's title, you have to call its setTitle:forState: method.
Also please note that using self inside the subscribeNext block is likely to create a retain cycle (and therefore a memory leak). You can read more about it in this answer. You can use #weakify / #strongify macros or, as mentioned in that answer, use rac_liftSelectors:withSignals: method (which IMHO seems to be cleaner).
Your code can be simplified as you actually don't need to split the signal at all. You can use a simple condition inside the map block and return the value which should be the button's title after it was pressed. This value will be sent as a next value of the resulting signal. You can also use startWith: operator to set the initial value (I guess it should be "Start").
RACSignal *buttonTextSignal = [[pressedStart map:^id(UIButton *buttonPressed) {
return [buttonPressed.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}]
startWith:#"Start"];
[self.start rac_liftSelector:#selector(setTitle:forState:) withSignals:buttonTextSignal, [RACSignal return:#(UIControlStateNormal)], nil];
What does rac_liftSelector:withSignals: do? Each time one of the signals sends its next value, it invokes the method identified by the selector (in this case setTitle:forState:). The method is invoked with next values of the signals as its parameters. So in our case it will initially call:
[self.startButton setTitle:#"Start" forState:UIControlStateNormal];
If you wanted to set a single property (let's say titleLabel.text), you could bind it with RAC macro:
RAC(self.startButton, titleLabel.text) = buttonTextSignal;
Unfortunately, it only works for setting properties, and in your case you have to call a method with two arguments, that's why you have to use rac_liftSelector:withSignals.
As I said, you could achieve the desired result using subscribeNext:
#weakify(self);
RACSignal *buttonTextSignal = [[[pressedStart map:^id(UIButton *buttonPressed) {
return [buttonPressed.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}]
startWith:#"Start"]
subscribeNext:^(NSString *title) {
#strongify(self);
[self.startButton setTitle:title forState:UIControlStateNormal];
}];
But as you can see, you should take extra care to avoid a retain cycle, using #weakify and #strongify macros.
I'm trying to refactor my iOS app into ReactiveCocoa and ReactiveViewModel and I'm struggling with trying to work out a couple of best practices.
I'll boil this down to a simple use case - I was to push a view controller which loads some data and shoves it into a table view. If the endpoint call fails for whatever reason, I want to display a view on screen with a retry button.
I currently have this working but it looks a bit filthy. I feel like there must be a better way - am I even doing this correctly?
In my ViewModel's init method, I'm creating my command, which is called as soon as the ViewModel becomes active.
// create the command to load the data
#weakify(self);
self.loadStationsCommand = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *(id input) {
#strongify(self);
return [RACSignal createSignal:^(RACDisposable *(id<RACSubscriber subscriber) {
// load data from my API endpoint
...
BOOL succeeded = ...;
if (succeeded) {
[subscriber sendNext:nil];
[subscriber sendCompleted:nil];
} else {
// failed
[subscriber sendError:nil];
}
return nil;
}
}];
// start the command when the ViewModel's ready
[self.didBecomeActiveSignal subscribeNext:^(id x) {
#strongify(self);
[self.loadStationsCommand execute:nil];
}];
In my UIViewController, I'm subscribing to the command via -
[self.viewModel.loadStationsCommand.executionSignals subscribeNext:^(RACSignal *loadStationsSignal) {
[loadStationsSignal subscribeNext:^(id x) {
// great, we got the data, reload the table view.
#strongify(self);
[self.tableView reloadData];
} error:^(NSError *error) {
// THIS NEVER GETS CALLED?!
}];
}];
[self.viewModel.loadStationsCommand.errors subscribeNext:^(id x) {
// I actually get my error here.
// Show view/popup to retry the endpoint.
// I can do this via [self.viewModel.loadStationsCommand execute:nil]; which seems a bit dirty too.
}];
I must have some misunderstanding as to how RACCommand works, or at the very least I feel I'm not doing this as cleanly as I can.
Why doesn't the error block on my loadStationsSignal get called? Why do I need to subscribe to executionCommand.errors instead?
Is there a better way?
It is a correct way to handle errors with RACCommand. As you can read in the docs, errors of the inner signal are not sent when using executionSignals:
Errors will be automatically caught upon the inner signals, and sent upon
`errors` instead. If you _want_ to receive inner errors, use -execute: or
-[RACSignal materialize].
You can also use RAC additions to UIButton and bind self.viewModel.loadStationsCommand to rac_command of the retry button.
There is a good article which explains RACCommand and shows some interesting patterns to be used with it.
In the snipped bellow, I want to change the enableness status of self.btnSave, that has a RACCommand defined.
The second block actually tries to change it based on another external condition (tableview selection).
It seams that the rac_command takes control of the enabled status of the button, and the only way to make it work, would be creating a signal to control the enable status (that I have no idea how to do it)
Do you have any idea?
I would like to keep using RAC for the button actions, but if it gets more complicated than I imagined, I will have to abandon it.
Thanks in advance.
#weakify(self);
self.btnSave.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
#strongify(self);
Album* lalbum = [NSEntityDescription
insertNewObjectForEntityForName:#"Album"
inManagedObjectContext:self.production.managedObjectContext];
lalbum.name = self.txtAlbumName.text;
[self.production addAlbunsObject: lalbum];
[self.production.managedObjectContext save: nil];
self.close();
return [RACSignal empty];
}];
[[self.txtAlbumName rac_textSignal] subscribeNext:^(NSString* text) {
#strongify(self);
if ([text isEqualToString:#""])
{
self.btnSave.enabled = NO;
} else
self.btnSave.enabled = YES;
}];
The point of RACCommand is that it lets you disable the button while you perform some long-running action. If you don't want that (and your question implies that you don't), just use normal target/action semantics. There are other libraries that give you nice block-based helpers for UIBarButtonItems, if that's what you're after.
Also, try this on for size:
RAC(self.btnSave, enabled) = [self.txtAlbumName.rac_textSignal map:^(NSString *text) {
return ![text isEqualToString:#""];
}];
I've been learning a lot about ReactiveCocoa but one thing still puzzles me: why does the signal block on RACCommand return a signal itself?
I understand the use cases of RACCommand, its canExecute signal and signal block, and how it can be hooked up to UI elements. But what case would there be ever for returning something other than [RACSignal empty]?
infoButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// Do stuff
return [RACSignal empty];
}];
There are exceptions to every rule, but generally you want all your "// Do stuff" to be captured by the returned signal. In other words, your example would be better as:
infoButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id input) {
return [RACSignal defer:^{
// Do stuff
return [RACSignal empty];
}];
}];
The direct benefit of this change is that, for the duration of "// Do stuff", your infoButton will be disabled, preventing it from being clicked/tapped until the returned signal has completed. In your original code, the "do stuff" is outside of the signal, and as such your button won't be disabled properly.
For work that doesn't have much latency, for example making UI changes in response to a button tap, then the enabled/disabled feature of RACCommand doesn't buy you much. But if the work is a network request, or some other potentially long running work (media processing for example), then you definitely want all of that work captured within a signal.
Imagine you have a command that should load list of items from network. You could use side effects in signal block or return a signal that would actually send these items. In the latter case you can do the following:
RAC(self, items) = [loadItems.executionSignals switchToLatest];
Also all errors sent by signal would be redirected to errors signal, so:
[self rac_liftSelector:#selector(displayError:)
withSignals:loadItems.errors, nil];
It's impossible with [RACSignal empty]-powered commands.
I have an example that might be helpful, though others might may be able to explain it better.
RACCommand Example
But basically, the way you have it with returning +empty it seems sort of pointless, as invoking the command will basically be using side effects, which we want to avoid.
Instead of using this:
infoButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id input) {
return [RACSignal defer:^{
// Do stuff
return nil;
}];
}];
You can use this built-in method to control the button state:
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;