I have a test example with one button. When user presses the button is called RAC_request and request is executing. If user presses this button a many times for a short time, many requests will be execute asynchronously. I want to create logic to previously signal cancelled when new request is executing by Reactive Cocoa. I know that exists switchToLatest in Reactive Cocoa, but I can't do that logic work correctly. How do this by RAC?
if user presses the button a many times for a short time, you can use throttle to set the time. if a interval times have many nextValue, it only take newest. also you can use switchToLatest. There is a easy example, i hole that would be useful for you.
[button.rac_command execute:nil];
button.rac_command = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:#"TestSignal"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
}];
[[[button.rac_command.executionSignals throttle:0.5] switchToLatest]subscribeNext:^(id x) {
NSLog(#"%#", x);
}];
Related
I've created a signal to wrap the UITextField Delegate Method textFieldShouldReturn:.
- (RACSignal *)textFieldReturnPressed
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[self rac_signalForSelector:#selector(textFieldShouldReturn:)
fromProtocol:#protocol(UITextFieldDelegate)]
subscribeNext:^(RACTuple *tuple) {
[subscriber sendNext:tuple.first];
}];
return nil;
}];
}
In viewDidLoad, I'm attempting to subscribe to the combination of this signal and a button press. In effect, I'd like a user to be able to tap a button and do some things (login), or press return on the keyboard and do the same things (login).
I've created the following signal to combine the two signals:
RACSignal *loginSignal = [RACSignal
combineLatest:#[[loginButton
rac_signalForControlEvents:UIControlEventTouchUpInside],
[self textFieldReturnPressed]]];
I then subscribe to this event like so:
[loginSignal
subscribeNext:^(id x) {
NSLog(#"%#", x);
}];
When I press the return key on the keyboard, the log above doesn't print (although I have verified sendNext is called from the signal). However, when I trigger the login button signal, the log prints ie. combineLatest passes through the signal.
I've experimented with adding startWith:nil to the loginButton signal, because, as I've found in other posts/Github issues, CombineLatest requires each signal to have been sent at least once, but my stream executes immediately.
I'm sure I could somehow filter at that point to prevent the stream from executing, but that feels a bit hacky. Any recommendations?
The reason why combineLatest isn't give the desired effect is that it needs every signal passed to send at least one next event for subscribers to start receiving next events.
To achieve the effect you want, i.e. two signals with each passing a next event irrespective of the other's state, you should merge: the signals.
Example:
RACSignal *loginSignal = [[loginButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:[self textFieldReturnPressed]];
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.
I'm trying to learn ReactiveCocoa and I'm writing a simple Space Invaders clone, based on a Ray Wenderlich tutorial.
Lately during the development, I faced an issue I can't resolve.
Basically I've two signals:
a tap gesture signal
a timed sequence that fires every second
What I want to achieve is to combine these signals in a new one, that fires when both the signals change:
is it possible?
I saw the combineLatest method, but the block is execute whenever any signals change.
My wanted pseudocode is:
RACSignal *updateEventSignal = [RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]];
RACSignal *gestureSignal = [[UITapGestureRecognizer new] rac_gestureSignal];
[[RACSignal combineBoth:#[gestureSignal, updateEventSignal]
reduce:^id(id tap, id counter){
return tap;
}]
subscribeNext:^(id x) {
NSLog(#"Tapped [%#]", x);
}];
Probably I can achieve the same result in other way or this is not the expected behaviour or ReactiveCocoa, but at this point I wonder if I'm in the right reactive track or not.
Instead of +combineLatest:reduce:, you want +zip:reduce:. Zip requires that all the signals change before reducing and sending a new value.
Since you don't actually care about the values from the timer, -sample: may do what you want:
[[gestureSignal
sample:updateEventSignal]
subscribeNext:^(id tap) {
NSLog(#"Tapped [%#]", tap);
}];
This will forward the latest value from gestureSignal whenever updateEventSignal fires.
[[[[RACSignal zip:#[RACObserve(self, minimum), RACObserve(self, maximum),
RACObserve(self, average)]] skip:1] reduceEach:^id{
return nil;
}] subscribeNext:^(id x) {
[self buildView]; //called once, while all three values were changed.
}];
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;
I want to implement a countdown timer using Reactive Cocoa in iOS. The timer should run for X seconds and do something in every second. The part I cannot figure out is the way I could cancel the timeout.
RACSubscribable *oneSecGenerator = [RACSubscribable interval:1.0];
RACDisposable *timer = [[oneSecGenerator take:5] subscribeNext:^(id x) {
NSLog(#"Tick");
}];
I think, I found the solution. The trick is to merge the cancel signal into the tick signal, then take X samples. The final subscribers will receive a next event every time the tick signal ticks and completed when the 'take' is finished. Cancellation can be implemented by sending error on the cancel timer.
__block RACSubject *cancelTimer = [RACSubject subject];
RACSubscribable *tickWithCancel = [[RACSubscribable interval:1.0] merge:cancelTimer];
RACSubscribable *timeoutFiveSec = [tickWithCancel take:5];
[timeoutFiveSec subscribeNext:^(id x) {
NSLog(#"Tick");
} error:^(NSError *error) {
NSLog(#"Cancelled");
} completed:^{
NSLog(#"Completed");
[alert dismissWithClickedButtonIndex:-1 animated:YES];
}];
To activate cancel, one has to do the following.
[cancelTimer sendError:nil]; // nil or NSError
There is also the TakeUntil operator which does exactly what you want: relays events from a stream until another one produces a value.