I'm trying to write a completion handler in IOS with a block and am not sure exactly why it's not working.
This is what I have so far
typedef void(^myCompletion)(BOOL);
-(void) showAnswer {
[self showAnswer:^(BOOL finished) {
if (finished) {
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
//once the tiles position have been updated then move to the next riddle
[scene nextRiddle];
}
}];
}
// This method should update the position of tiles on the scene
-(void) showAnswer:(myCompletion) compblock{
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
NSArray *tilesArray = [scene.tilesBoundary children];
for (Tile *tile in tilesArray) {
if (tile.positionInAnswer != 17) {
[tile removeFromParent];
[scene.spacesBoundary addChild:tile];
CGPoint targetPoint = CGPointMake(tile.targetPoint.x, tile.targetPoint.y + 6);
tile.position = targetPoint;
}
}
compblock(YES);
}
And I am calling the showAnswer method from a Scene as follows:
GameViewController *gVC = (GameViewController *)self.window.rootViewController;
[gVC showAnswer];
As I step through the code I don't encounter any errors and the sequence proceeds as expected ie. The tile positions are changed and then the completion handler triggers to the nextRiddle method. Only problem is that none of the interface is updated. Is there something I am missing?
I've tested that the tile repositioned code works by taking out the completion block and I view it in the interface and get the desired results. So I'm pretty sure the problem lies in how I'm implementing the blocks. I've never written a block before so I'm really in the dark.
-(void) showAnswer {
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
NSArray *tilesArray = [scene.tilesBoundary children];
for (Tile *tile in tilesArray) {
if (tile.positionInAnswer != 17) {
[tile removeFromParent];
[scene.spacesBoundary addChild:tile];
CGPoint targetPoint = CGPointMake(tile.targetPoint.x, tile.targetPoint.y + 6);
tile.position = targetPoint;
}
}
}
I think the problem seems to be a conceptual mistake - that the "flow" of code
in the -(void) showAnswer:(myCompletion) comp block method truthfully represents the flow of UI elements in time in the presentation layer.
However in practice it is not that straightforward. The UI layer is rendered with 60 Hz framerate. That means you see 60 (frames) screens per second and contents of each of those screens needs to be calculated in advance, processed, flattened, rendered.
That means 1 frame (screen) cycle/pass lasts approx. 16 milliseconds. If I remember correctly you as a programmer have 11 milliseconds to provide all the relevant code so that it can be processed into visual info. This chunk of code is processed all at once which is the answer to our problem.
Imagine if you had a method that would say
{
//some other UI code...
self.view.backgroundColor = [UIColor greenColor];
self.view.backgroundColor = [UIColor yellowColor];
//some other UI code...
}
Now imagine we are inside 1 such 16 millisecond pass.
This piece of code would be processed in advance before any rendering happens and the resultative background colour will be yellow. Why? Because in the cycle preparation phase this code is processed and the framework interprets the green colour as pointless because the value is immediately overwritten by a yellow one.
Thus instructions for the renderer will contain only the yellow background information.
Now back to your actual code. I think all your code is processed fast enough to fit into that 11 milisec window, and the problem is that you do not really wait for the swapping of tiles because there is that block with YES parameter as part of the method and that one already slides to whatever is next.
So the rendered does get instructions to simply move on the the next thing. And it does not animate.
Try putting this "tile swapping" into the "animation" block
[UIview animateWithDuration:....completion] method..and put your block into the completion block. Yes you will have a block in a block but thats fine.
[UIView animateWithDuration:0.4f
animations:^{
//tile swapping code
}
completion:^(BOOL finished)
{
complBlock(YES);
}];
I am not completely sure if it will work but let's try.
The UI relate method should call on main thread. U can try this:
[self showAnswer:^(BOOL finished) {
if (finished) {
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
dispatch_async(dispatch_get_main_queue(), ^{
[scene nextRiddle];
});
}
}];
Related
Can someone please tell me why this doesn't work?:
//Changing the Images
- (void) squareOneColour {
NSUInteger r = arc4random_uniform(5);
[self.squareOne setBackgroundImage:[self.colorArray objectAtIndex:r] forState:UIControlStateNormal];
}
//Moving buttons
- (void) squareOneMover {
NSUInteger r = arc4random_uniform(3);
[self.squareOne setHidden:NO];
CGPoint originalPosition = self.squareOne.center;
originalPosition.y = -55;
[self.squareOne setCenter:originalPosition];
CGPoint position = self.squareOne.center;
position.y += 760;
[UIView animateWithDuration:r + 3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
[self.squareOne setCenter:position];
}
completion:^(BOOL complete) {
if (complete) {
[self squareOneColour];
[self squareOneMover];
}
}
];
}
I am trying to get a UIButton to animate down the screen and when the animation is finished, for it to repeat but with a different background. The code above seems it should work but the second animation is never completed. (The UIButton is animated once, then everything stops). I have tried putting the code shown below (into the completion block), with an outside NSUInteger method creating a random number for r but it never works. But it does work when I replace r with a different number, like 1 or 2.
I found the answer. After playing around with it, I found that the function I wanted the UIButton to do was only fit for a UIImageView. I guess I'll have to make an invisible button over the UIImageView
You're stepping on your own feet. Step out to the next cycle of the runloop and all will be well:
[self squareOneColour];
dispatch_async(dispatch_get_main_queue(), ^{
[self squareOneMover];
});
I am running a sprite effect in an SKScene at certain points. When the effect is not running I want to pause the scene as otherwise it takes up additional CPU even with no effect running. I was not able to find a delegate that tells me when the effect stops rendering so that I can pause the scene again. I could brute force it by having it wait a safe time, but is there a more elegant way to know when the effect is finished?
Setup in viewWillAppear:
self.skScene = [EffectsScene sceneWithSize:self.view.frame.size];
[self.skScene setBackgroundColor:[UIColor whiteColor]];
[self.skView presentScene:self.skScene];
[self.skView setPaused:YES];
Method that gets called to run the effect at a specific time:
-(void)deleteEffect
{
if (shouldRunDeleteEffect)
{
[self.skScene smokeEffectAtX:self.view.frame.size.width/2 andY:0];
[self.skView setPaused:NO];
shouldRunDeleteEffect = NO;
}
}
The actual scene details:
-(void)smokeEffectAtX:(float)x andY:(float)y
{
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"SmokeEffect" ofType:#"sks"]];
emitter.position = CGPointMake(x,y);
emitter.numParticlesToEmit = 1000;
[self addChild:emitter];
}
This method creates an SKEmitterNode, calculates how long the emitter will take to run based on its properties and the duration parameter, and adds an SKAction to the emitter to remove it from the scene after it is done running.
- (void) newSmokeNodeAtPosition:(CGPoint)position withDuration:(NSTimeInterval)duration
{
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"SmokeEffect" ofType:#"sks"]];
emitter.position = position;
// Calculate the number of particles we need to generate based on the duration
emitter.numParticlesToEmit = duration * emitter.particleBirthRate;
// Determine the total time needed to run this effect.
NSTimeInterval totalTime = duration + emitter.particleLifetime + emitter.particleLifetimeRange/2;
// Run action to remove the emitter from the scene
[emitter runAction:[SKAction sequence:#[[SKAction waitForDuration:totalTime],
[SKAction removeFromParent]]]
completion:^{
// Add code here to run when emitter is done
self.scene.paused = YES;
}];
[self addChild:emitter];
}
I've written a method, sortAndSlideHandCards(), that moves 6 UIButtons. Each UIButton is moved to the same position. This is done via a for-each loop and the animateWithDuration method being called on each UIButton.
This method is called for a number of players at the same time. Currently the behaviour results in UIButtons from each player moving but only one at a time. No more than one UIButton can move at any time, as if each animation is waiting for whatever animation that is currently running to stop before attempting it's own animation, essentially the code is executed sequentially for each player/UIButton. I hoped threading would help me fix this.
when I added the threading code:
backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
sortAndSlideHandCardsGroup = dispatch_group_create();
for(Player* player in _playersArray) {
dispatch_group_async(sortAndSlideHandCardsGroup, backgroundQueue, ^(void) {
[player sortAndSlideHandCards];
});
dispatch_group_wait(sortAndSlideHandCardsGroup,DISPATCH_TIME_FOREVER);
I found that only the first UIButton animation is triggered for each player and that the code gets held up in the runloop "while" because "_animationEnd" never gets set as it would appear the second animation never gets going.
I can see the method launching in its own thread
- (void) sortAndSlideHandCards {
NSLog(#"PLAYER:sortAndSlideHandCards");
CGPoint newCenter;
Card* tempCard = nil;
int count = 1;
float duration = 0.2 / _speedMultiplyer;
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for(Card *card in _handCards) { //move cards in hand to one postion in hand
if(count == 1) {
tempCard = [[Card alloc] init:_screenWidth:_screenHeight :[card getNumber] :[card getCardWeight] :[card getSuit] :[card getIsSpecial]];
[tempCard setImageSrc: _playerNumber :!_isPlayerOnPhone :count : true :_view: _isAI: [_handCards count]];
newCenter = [tempCard getButton].center;
}
_animationStillRunning = true;
if(![[DealViewController getCardsInPlayArray] containsObject:card] ) {
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionLayoutSubviews animations:^{[card getButton].center = newCenter;} completion:^(BOOL finished){[self animationEnd];}];
while (_animationStillRunning){ //endAnimation will set _animationStillRunning to false when called
//stuck in here after first UIButton when threading code is in play
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
} //endAnimation will set _animationStillRunning to false when called
}
count++;
}
}
When i comment out the threading code each UIButton (Card) will animate one after another.
With the threading code is in play the first UIButton will animate but during the second run through the for-loop, the code will be stuck in the while-loop, waiting for the animation to end. I'm guessing the second animation doesn't even start.
I also tried this for the threading code:
[player performSelectorInBackground:#selector(sortAndSlideHandCards) withObject:nil];
Same outcome
Anyone have any ideas why animateWithDuration doesn't like getting called in a loop when in a thread other than the main one?
You should just be able to kick off the animations you want from whatever UI action is performed. The UIView animateWith... methods return immediately, so you don't need to worry about waiting for them to complete.
If you have an unknown number of animations to kick off sequentially, use the delay parameter. Pseudocode:
NSTimeInterval delay = 0;
NSTimeInterval duration = 0.25;
for (UIView *view in viewsToAnimate)
{
[UIView animateWithDuration:duration delay:delay animations:^{ ... animations ...}];
delay += duration;
}
This will increase the delay for each successive animation, so it starts at the end of the previous one.
The following code has a (at least one) deep flaw: Once CGFrame is set it's location is fixed.
-(UIImageView*) ownImageView {
if (ownImageView == nil) {
ownImageView = [[UIImageView alloc] initWithFrame:
CGRectMake(self.xPosition, self.yPosition,
[constants cardWidth], [constants cardHeight])];
[ownImageView setImage:self.faceImage];
}
return ownImageView;
}
I would like to reuse the UIImageView (once created, I don't want to realloc it). Instead, i'd like to change x,y coordinates of the frame it contains.
The following code works fine, however, it seems carelessly wasteful
-(UIImageView*) ownImageView {
[ownImageView removeFromSuperview];
ownImageView = [[UIImageView alloc] initWithFrame:
CGRectMake(self.xPosition, self.yPosition,
[constants cardWidth], [constants cardHeight])];
[ownImageView setImage:self.faceImage];
return ownImageView;
}
Question: How would you handle this, while keeping as much of lazy instantiation as possible?
The view represents a 2d rectangle moving along the screen. I'd like to represent the "move" by simply changing X/Y position of the existing view instead of replacing it with a new copy.
ownImageView.frame = CGRectMake(x,y,w,h);
If you want to move it the simplest thing to do is to let Core Animation do the work for you.
For more details and options check UIView Class Reference
[UIView animateWithDuration:2
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^{
self.THE_VIEW.center = CGPointMake(200, 200);
// OR you can change it's frame if you prefer
}
completion:NULL];
I want to use UIActivityIndicatorView on my custom UIButton.
Here is my code:
if (sender.tag == 1)
{
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
if ([self reachable]) {
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}
}
What I want to do here is:
Once the user touch the button, I want to start the ActivityIndicatior while Reachable checking the network availability. Once it done pass it to the next view.
Update
UIActivityIndicator is on top of my custom UIButton. It build successfully, but ActivityIndicator is not showing when I touch the button.
I'm sure you had your answer since then.
After 4 years, language has changed to Swift, but I had the same problem : UIActivityIndicatorView is not showing when I add it through :
self.myButton.addSubview(self.myIndicatorView)
I had to climb a level up :
self.myButton.superview!.addSubview(self.myIndicatorView)
Assuming everything else is correct in your project, the problem here is that you're showing and hiding an activity indicator in the same event loop, without giving a pause to draw it. Let me explain:
If you have the code:
UIView* view = someView;
view.backgroundColor = [UIColor redColor];
// various synchronous operations
view.backgroundColor = [UIColor yellowColor];
The view will only ever have a background color of yellow.
To answer your question, you probably want to animate the spinner, do some asynchronous operation, then stop the animation. The key being to not stall on the main event loop while your asynchronous task is running. If your comfortable with blocks it would look something like this:
if (sender.tag == 1) {
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
[self checkReachableWithCallback:^{
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}];
}