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.
Related
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);
}];
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 new to ReactiveCocoa and there is a problem I couldn't yet find a way to solve. I have a network request in my app which returns data to be encoded in a QR code that will be valid for only 30 seconds. The network request returns a RACSignal and I send the data to be encoded in that signal to my view model. In the view model I map that data to a QR image and expose it as a property in my view model interface. After I create the QR image, I want to update a timeLeftString property that says "This code is valid only for 30 seconds" but the seconds will change as time progresses, and after that 30 seconds complete, I want to make another request to fetch another QR code data that will be valid for 30 seconds more and after that completes another request the fetch data that will be valid for 30 seconds...up until the screen is dismissed. How do I go about implementing this?
Currently I have this to get the data:
- (RACSignal *)newPaymentSignal
{
#weakify(self);
return [[[[APIManager sharedManager] newPayment] map:^id(NSString *paymentToken) {
ZXMultiFormatWriter *writer = [ZXMultiFormatWriter writer];
ZXBitMatrix *result =
[writer encode:paymentToken format:kBarcodeFormatQRCode width:250 height:250 error:nil];
if (!result) {
return nil;
}
CGImageRef cgImage = [[ZXImage imageWithMatrix:result] cgimage];
UIImage *image = [UIImage imageWithCGImage:cgImage];
return UIImagePNGRepresentation(image);
}] doNext:^(NSData *data) {
#strongify(self);
self.qrImageData = data;
}];
}
and this for timer
- (RACSignal *)timeRemainingSignal
{
#weakify(self);
return [[[RACSignal interval:0.5 onScheduler:[RACScheduler scheduler]] //
startWith:[NSDate date]] //
initially:^{
#strongify(self);
self.expiryDate = [[NSDate date] dateByAddingTimeInterval:30];
}];
}
The flow is: get data from the api, start the timer, and when the time is up make a new request to get new data and start timer again..and repeat this forever.
1- How do I start the timer after I get data from the API?
2- How do I make this flow repeat forever?
3- How do I stop the timer before 30 seconds complete and start the flow from the beginning if the user taps a button on the user interface?
4- I have an expiryDate property which is 30 seconds added to current date because I thought I will take the difference of expiryDate and [NSDate date] to decide whether the time is up - is there a better way to implement this?
5- How do I break the flow when it's repeating forever and unsubscribe from everything when the screen is dismissed (or, say, when user taps another button)?
thanks so much in advance for the answers.
I think the missing piece of the puzzle is the very useful flattenMap operator. It essentially replaces any nexts from its incoming signal with nexts from the signal returned by it.
Here's one approach to solving your problem (I replaced your newPaymentSignal method with a simple signal sending a string):
- (RACSignal *)newPaymentSignal
{
return [[RACSignal return:#"token"] delay:2];
}
- (void)start
{
NSInteger refreshInterval = 30;
RACSignal *refreshTokenTimerSignal =
[[RACSignal interval:refreshInterval onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]];
[[[[refreshTokenTimerSignal
flattenMap:^RACStream *(id _)
{
return [self newPaymentSignal];
}]
map:^NSDate *(NSString *paymentToken)
{
// display paymentToken here
NSLog(#"%#", paymentToken);
return [[NSDate date] dateByAddingTimeInterval:refreshInterval];
}]
flattenMap:^RACStream *(NSDate *expiryDate)
{
return [[[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[refreshTokenTimerSignal skip:1]]
map:^NSNumber *(NSDate *now)
{
return #([expiryDate timeIntervalSinceDate:now]);
}];
}]
subscribeNext:^(NSNumber *remaining)
{
// update timer readout here
NSLog(#"%#", remaining);
}];
}
Every time the outer refreshTokenTimerSignal fires, it gets mapped to a new newPaymentSignal, which in turn when it returns a value gets mapped to an expiry date, which is used to create a new "inner" timer signal which fires every second.
The takeUntil operator on the inner timer completes that signal as soon as the outer refresh timer sends a next.
(One peculiar thing here was that I had to add a skip:1 to the refreshTokenTimerSignal, otherwise the inner timer never got started. I would have expected it to work even without the skip:1, maybe someone better versed in the internals of RAC could explain why this is.)
To break the flow of the outer signal in response to various events, you can experiment with using takeUntil and takeUntilBlock on that too.
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;
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.
}];