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
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 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.
I've a player of Sprite * type in cocos2dx V3, I want it to run different animation on different time interval, I could not find method to pause and then resume a specific animation(Action). Although I can Pause and Resume all actions of Sprite Simultaneously using _player->pauseSchedulerAndActions().I'm using "CCRepeatForever" actions on sprite, so, I must have to pause one to resume other.Please help to Pause an action by tag or by any other method.
Thanks In Advance.
Oops
I made the assumption that this was Objective-C but #Droppy has informed me that it is not.
I didn't realise cocos2d-x was different. However, because this is a fairly high level framework the concept behind what I've done in the answer will still work. I'll keep the answer here for now.
The answer
It's been a while since I've done any Cocos2D stuff but I can give you the idea.
Instead of creating an action and repeating it forever you should have a method something like this...
- (void)twirlAround
{
// only create and perform the actions if variable set to YES
if (self.twirling) {
// this will do your action once.
CCAction *twirlAction = // create your twirl action (or whatever it is)
// this will run this function again
CCAction *repeatAction = [CCActionCallBlock actionWithBlock:^{
[self twirlAround];
}];
// put the action and method call in sequence.
CCActionSequence *sequence = [CCActionSequence actions:#[twirlAction, repeatAction]];
[self runAction:sequence];
}
}
This will run repeatedly as long as the twirling property is set to YES.
So, somewhere else in your code (probably where you are currently adding your repeating action) you can do this...
self.twirling = YES;
[self twirlAround];
This will start the repeated twirling.
To stop it you can then do...
self.twirling = NO;
This will stop the twirling.
Alternative method
- (void)twirlAround
{
// this will do your action once.
CCAction *twirlAction = // create your twirl action (or whatever it is)
// this will run this function again
CCAction *repeatAction = [CCActionCallBlock actionWithBlock:^{
if (self.twirling) {
[self twirlAround];
}
}];
// put the action and method call in sequence.
CCActionSequence *sequence = [CCActionSequence actions:#[twirlAction, repeatAction]];
[self runAction:sequence];
}
based on Fogmeister advice, this is cocos2d-x version of that
void MySprite::jumpForever(){
if (!twirling) return;
auto jump = JumpBy::create(0.5, Vec2(0, 0), 100, 1);
auto endCallback = CallFuncN::create(CC_CALLBACK_1(MySprite::jumpForever,this));
auto seq = Sequence::create(jump, endCallback, nullptr);
runAction(seq);
}
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'm trying to continuously check the readings from the accelerometer before the camera takes a picture. I have a function that takes a picture, and at the beggining of that function I check whether the accelerometer readings are too high. If they are, I'd like to call the function again to check if acceleration has stopped.
- (void)takePicture {
if (accelerating == YES) {
[self takePicture];
}
else {
// Code that takes picture
}
}
I guess the problem I'm having is the function gets recursively called too many times that I get "EXC_BAD_ACCESS (code=2)". How can I fix this recursive calling problem?
Calling [self takePicture] that will end up retrying immediately, which is useless and wasteful, as you're not even giving your application any time to receive additional accelerometer events. The value of accelerating will probably never change before you run out of stack.
What you probably want to do here is have the method called a fraction of a second later, e.g.
[self performSelector: #selector(takePicture) withObject:nil afterDelay: 0.01];
return;
This will have the event loop call your method again 10 milliseconds (0.01 second) later.
A potential better way of implementing this would be by overriding the setter for accelerating to something like this:
- (void)setAccelerating:(BOOL)accelerating {
_accelerating = accelerating;
if (accelerating) // maybe some more conditions, but you get the idea
[self takePicture];
}