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.
Related
I have created a UIButton and on click event, I am showing an image in the web view. Also, I am refreshing the image in every 30 sec. But when I click on button multiple times, refresh method get called multiple time as well.
I want it to work like, It saves last click time and refreshes as per that time instead of multiple times.
What can I do for it?
I tried to kill all previous thread instead of the current thread but that's not working.
Please help if anyone already know the answer.
Below is my image refresh code:
- (void)refreshBanner:(id)obj {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (![SNRunTimeConfiguration sharedInstance].isInternetConnected) {
[self removeBannerAdWithAdState:kADViewStateNotConnectedToInternet];
return;
}
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
self.bannerPaused = YES;
return;
}
self.adView.hidden = YES;
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
topController = [SNADBannerView topViewControllerWithRootViewController:topController];
if ([self checkInViewHierarchy:self parentView:topController.view]) {
// NSLog(#"Visible View Is: %#", self.adId);
SNADMeta *meta = [[SNADDataBaseManager singletonInstance] adToShowWithBanner:YES excludeTyrooAd:YES audio:NO zoneId:self.adSoptZoneId fixedView:NO condition:nil contextualKeyword:nil onlyFromAJ:NO];
SNADAdLocationType type = SNADAdLocationTypeHeader;
if (self.bannerType == SmallViewTypeFooter) {
type = SNADAdLocationTypeFooter;
}
if (self.isFromCustomEvent) {
type = SNADAdLocationTypeAdMobBanner;
}
NSString *message = meta ? nil : kSNADOppMissReason_NoAdToShow;
[SNRunTimeConfiguration fireOpportunityForAdLocation:type zoneId:self.adSoptZoneId reason:message];
NSLog(#"******************* Opportuninty fired for refresh banner ***************************");
if (meta) {
self.meta = meta;
[self updateContentForWebAd:nil];
[self updateStatsForAd];
//fireImpression
[SNADBannerView fireImpression:self.meta];
if ([meta.adSource isEqualToString:kSNADParameter_APC]) {
self.sdkMediation = [[SdkMediation alloc] init];
[self.sdkMediation fireTrackingAdType:self.meta.type isFill:YES];
}
// Ad Height Delegate.
if ([self.meta.displayType isEqualToString:kSNADDisplayType_web]) {
self.adHeightDelegateCalled = YES;
NSInteger height = self.meta.height.integerValue;
self.bannerCH.constant = height;
if ([self.callBackDelegate respondsToSelector:#selector(adWillPresentWithHeight:adId:adType:)]) {
[self.callBackDelegate adWillPresentWithHeight:height adId:self.adId adType:SeventynineAdTypeMainStream];
}
}
} else {
[self removeBannerAdWithAdState:kADViewStateNoAdToShow];
if ([meta.adSource isEqualToString:kSNADParameter_APC]) {
[self.sdkMediation fireTrackingAdType:self.meta.type isFill:NO];
}
return;
}
} else {
// NSLog(#"View Which Is Not Visible Now: %#", self.adId);
}
SNAdConfiguration *configuration = [SNAdConfiguration sharedInstance];
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:configuration.autoRefRate target:self selector:#selector(refreshBanner:) userInfo:nil repeats:NO];
}];
}
Use GCD, and not NSOperationQueue.
Then you step away from your immediate task. You do lots and lots of complicated things inside refreshBanner. And you will do more complicated things to make it work when the user taps multiple times.
Think about what exactly you need. Abstract the "refresh automatically, and when the button is clicked, but not too often" into a class. Then you create a class that takes a dispatch_block_t as an action, where a caller can trigger a refresh anytime they want, and the class takes care of doing it not too often. Then you create an instance of the class, set all the needed refresh actions as its action block, refreshBanner just triggers a refresh, and that class takes care of the details.
You do that once. When you've done it, you actually learned stuff and are a better programmer than before, and you can reuse it everywhere in your application, and in new applications that are coming.
NSOperationQueue have cancelAllOperations method. But for the main queue it's not a good decision to use this method, cause main queue is shared between different application components. You can accidentally cancel some iOS/other library operation together with your own.
So you can create NSOperation instances and store them in an array. Then you can call cancel for all scheduled operations by iterating trough this array, and it will only affect your operations.
Note that block operations doesn't support cancellation. You will need to create your own NSOperation subclass, extract code from your execution block into that subclass main method. Also, you'll need to add [self isCancelled] checks that will abort your logic execution at some points.
I forgot to mention that currently your execution block is fully performed on the main queue. So, you'll need to move any heavy-lifting to background thread if you want to cancel your operation in the middle of processing from main thread.
I need to add that I agree with #gnasher729 - this doesn't look like an optimal solution for the problem.
I have resolved the issue.
Multiple threads created because a new view is created every time I call the API to display image. So now I am removing views if any available before displaying image, then only last object remains and refresh is called as per last called time.
Every View has it's own object that's why multiple threads has created.
By removing views my issue has been resolved.
Thanks everyone for replying.
I'm trying to wrap my head around the ReactiveCocoa framework, but I'm stuck on trying to figure out how to delay conditionally.
For example, I want to set a CADisplayLink pause property to false when an array is empty. Here is how I accomplished this :
RACSignal *changeSignal = [self rac_valuesAndChangesForKeyPath:#keypath(self, projectiles) options:NSKeyValueObservingOptionNew observer:nil];
RAC(self.displayLink, paused) = [changeSignal map:^id(RACTuple *value) {
return #([((NSMutableArray *)value.first) count] == 0);
}];
But before I pause the display link, I want to keep animating for a few seconds, so I added a delay:2.5]; to the end of the map block.
Now I'm running into the problem that it's waiting 2.5 seconds to stop AND start the display link. I only want RAC to pause when I'm setting the self.displayLink.paused to YES but not when I'm setting it to NO.
Is this type of "conditional delay" possible in ReactiveCocoa, and if so, how is it done?
I got some help at the GitHub page for ReactiveCocoa :
You can use -flattenMap: to do this since it lets you return a signal instead of just a single value:
RAC(self.displayLink, paused) = [changeSignal flattenMap:^id(RACTuple *value) {
RACSignal *pauseSignal = [RACSignal return:#([((NSMutableArray *)value.first) count] == 0)];
if (pause) {
return [pauseSignal delay:2.5];
} else {
return pauseSignal;
}
}];
So when we're pausing, we delay 2.5 seconds and then pause. When we're unpausing we immediately send the value through
I'm building a "monitoring" app on my iPhone. I'm using AFNetworking-2.0. I have a backend server exposing a RESTful interface written in Python3/tornado.
Depending on what level of ViewController I'm at, I want to poll different data with different queries (the focus of the application tunes the focus of the queries). In the interest of "Make it Work", I have set up the following:
#pragma mark - Pull Loop
- (void) forkPull {
NSString* uri = [NSString stringWithFormat: #"%#/valves", Site.current.serialID];
[[HttpConnection current]
GET: uri
parameters: #{}
success:^(NSURLSessionDataTask* task, id responseObject){
[Site.current performSelectorOnMainThread: #selector(fromDoc:) withObject:responseObject waitUntilDone:YES];
NSTimeInterval delay = 60; // default poll period
// attempt to hone in if we have valid lastTouch info
if (Site.current.touched != nil) {
NSDate *futureTick = [Site.current.touched dateByAddingTimeInterval: 65];
if ([futureTick compare: [NSDate date]] == NSOrderedDescending) {
delay = futureTick.timeIntervalSinceNow;
}
}
[self performSelector: #selector(forkPull) withObject:nil afterDelay:delay];
NSLog(#"%# forkPull again in %f", self, delay);
}
failure:^(NSURLSessionDataTask* task, NSError* error){
NSLog(#"%# forkPull error: %# (uri=%#)", self, error, uri);
[self performSelector: #selector(forkPull) withObject:nil afterDelay:60];
}
];
}
- (void) stopPull {
[NSObject cancelPreviousPerformRequestsWithTarget: self];
}
#pragma mark - View Management
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear: animated];
....
[self forkPull]; // start up polling while I'm visible
}
-(void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self stopPull]; // I'm going away, so shut down the pull loop?
...
}
Basically, when the controller's view appears, it sends a REST query (when it gets back asynchronously, it will update the model in the fromDoc: methods; the controller has KVO relationships set up which will cause UI changes. After the update completes, it's able to approximate about when it should make the next pull, and schedules that with performSelector:withObject:afterDelay:. When another controller takes center stage, the viewWillDisappear: method attempts to stop any forkPulls that have been queued.
While this kinda works. I'm pretty sure it doesn't pass the "Make it Right" test. I'm naive about how all the tasks and backgrounding work, but it seems to me that AFNetworking adds its own level of them, so my stopPull might not be effective. I've seen some evidence of that with my NSLog output, where it seems that controllers that aren't on the top anymore, still have loops running.
But I'm sure others have done this kind of pattern before. I'd love to know how to better architect/implement this. I'm looking for someone to share the pattern they've used for doing the semi-periodic REST queries, that has been vetted and works well.
Use Grand Central Dispatch:
#property (strong, nonatomic) dispatch_source_t timer;
- (void)startTimer
{
if (!self.timer) {
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
}
if (self.timer) {
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), 60ull*NSEC_PER_SEC, 10ull*NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^(void) {
[self tick];
});
dispatch_resume(_timer);
}
}
- (void)tick
{
// Do your REST query here
}
This will invoke your tick method every 60 seconds.
To suspend and resume your timer, use dispatch_suspend and dispatch_resume:
dispatch_suspend(self.timer);
dispatch_resume(self.timer);
You can invoke dispatch_source_set_timer at any time later to schedule ticks sooner or delay them until later:
// Fire sooner than 60 seconds, but resume 60s fires after that
unsigned long long delaySeconds = arc4random() % 60;
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, delaySeconds * NSEC_PER_SEC), 60ull*NSEC_PER_SEC, 10ull*NSEC_PER_SEC);
See the Apple Concurrency Programming Guide for full docs on this.
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 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.