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.