I've added an additional CCLayer to my "GameScene" that becomes visible ([self addChild:_congratsScreen]) whenever my character collects a given amount of objects on the screen.
Within my GameScene.h I've declared my child layer (CClayer *congratsScreen) and I'm synthesizing it on my GameScene.m. I'm allocating the child CCLayer in the GameScene's init method so it is holding the reference to the child layer in this instance variable.
On my GameScene I have a few CCParticleSystemQuad instances, and it's super simple to invoke both stopSystem and resetSystem to replay my particles animation, but if I try to do the same thing on the CCParticleSystemQuad that was initialized on the child layer, the resetSystem doesn't work after I remove the child from my GameScene and add it back again. Does something happens with the CCLayer's components once it is removed from a parent layer's scene?
I don't have the code at the moment so I will try to write some pseudo-code to illustrate how it's being done:
How it is being initialized on ChildLayer.m:
_sparkling= [CCParticleSystemQuad particleWithFile:#"sparkling.plist"];
Then, somewhere on GameScene.m I have:
- (void) showCongrats {
//pathetic way to create a modal panel
[self setTouchable = NO];
[[[self _congratsLayer] _sparkling] resetSystem];
[self addChild:_congratsLayer];
}
- (void) hideCongrats {
//let them continue playing
[self setTouchable = YES];
[[[self _congratsLayer] _sparkling] stopSystem];
[self removeChild:_congratsLayer];
}
So, it works on the first time I invoke showCongrats, the reference is good and I can manipulate the particles, but once I hide the layer, continue playing the game and show the congratulations panel again, it shows a frozen animation of the particles from the last invocation, the resetSystem no longer works. Any ideas?
I would add some breakpoints in the code and walk through it but if I had to guess I would say that when you are calling removeChild you are losing the data that you had in your init method and something funky is happening.
Related
Maybe this is NOT a duplicate question, as I have searched and tried many solutions about how to release objects under ARC.
The code is simple:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self recreateView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
[self.view addGestureRecognizer:tap];
}
- (void)tapped:(UITapGestureRecognizer *)g
{
[self recreateView];
}
- (void)recreateView
{
#autoreleasepool {
for (UIView *v in self.view.subviews) {
[v removeFromSuperview];
}
MyView *vv = [[MyView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:vv];
}
[self _performHeavyWork];
}
- (void)_performHeavyWork
{
int j = 0;
for (int i = 0 ; i < 100000000; ++i) {
j += random() % 7;
j = j % 18747;
}
}
#end
ViewController simply add a tap gesture recognizer whose action is to remove the old subview before adding a new one. MyView is a subclass of UIView which simply log a message when dealloced.
#implementation MyView
- (void)dealloc
{
NSLog(#"dealloc");
}
#end
The only magic is that the -_performHeavyWork is called every time a new view is created. When you keep on tapping on the screen quickly, the ViewController will be busy creating and discarding views. However, the odd thing is that all the discarded views are not dealloc immediately, but at the time you have stopped tapping for a while.
This is the profile of the process:
As you can see, the memory keep growing if you keep on tapping and so many of MyView instances exist at the same time. And if you comment out [self _performHeavyWork];, everything will be back to normal. So my question is:
Why do this happen?
And how can I solve it?
I think the main problem is that you're performing heavy work on the main thread. If you put the hard stuff on a different thread (or GCD) you'll probably see what you're expecting.
Here's some speculation on what's happening.
iOS responds to changes in the UI exclusively on the main thread. So if you're using the main thread for something else, the taps get queued for later processing.
You tap the screen, the main thread starts processing your heavy work.
You tap the screen some more. iOS can't deal with your request so it queues the event.
Eventually your heavy work completes and returns control to iOS.
iOS takes the queue of events and processes them all in a single run loop, which means the main loops auto release pool is never drained.
But what about the manual auto release pool? Well, all UI related stuff happens on the main loop and on the main thread, so the removeFrmSuperview: won't happen until control returns to the OS. Until that happens, the view hierarchy still holds a reference to your views, hence the memory growth.
You should never rely on dealloc being called when you think the last reference is gone. It is quite possible that references are still there where you don't expect them, but most importantly, dealloc can be called by ARC on a background thread.
First, iOS keeps a reference to a view when you add it to the visible window's view hierarchy. So, when you create a new instance your UIView subclass in the block, it remains in memory beyond the autorelease block. Second, the call to removeFromSuperview: does not actually result in the view being released by ARC until the main thread completes, which means there is still a reference to the view after the autorelease block ends. The work you're performing delays the main thread. This delays removing the final reference to the view.
Also, the autorelease block will not help in the case of removing the view because the view in question was not allocated in the same instance of the autorelease block. IOW, the view being created and added to the view hierarchy is not in the same scope when being removed later in the same block. So, there is no benefit to having the remove call in the autorelease block.
I have an issue that I'm stuck on, but I have no idea why it even happens; If I push a detail controller on the stack, and I swipe back very quickly using the default left edge interactivePopGestureRecognizer, my parent/root view controller's UINavigationBar looks corrupt or something, almost like the built in iOS transition mechanism didn't have time to do it's job at resetting it after the detail view is gone. Also to clarify, everything in this 'corrupt' UINavigationBar is still touchable and everything on my parent/root view controller works perfectly.
For people downvoting due to no source code: there is no source code! This is an Apple bug!
Is there anyway to reset this UINavigationBar to what it should be when the parent/root view controller's viewDidAppear method gets called?
Note that this bug does not occur if I tap the top left back button instead of using the left edge interactivePopGestureRecognizer.
Edit: I added an NSLog to check the navigationBar's subview count on viewDidAppear on the parent/root view controller, and the count is always the same, corrupt or not, so I'd like to know why the popped controller is wreaking havoc with my UINavigationBar.
If you can help me at all, I'd greatly appreciate it! Thank you.
I've attached a screenshot of what it looks like: Note that the back chevron isn't part of my parent/root view controller, it's part of what was popped off the stack. Testing123 is the title for the parent/root view controller and not that of what was popped off the stack. The head and gear icons are part of the parent/root view controller.
Edit: I've thought something like this could fix the issue, but turns out it doesn't, and is really bad experience IMO too. This is not the kind of solution I'm looking for. I'm posting a large bounty so this can be resolved correctly! 😃. I just can't have this weird UI behavior be in a production quality app.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.navigationController pushViewController:[UIViewController new] animated:NO];
[self.navigationController popToRootViewControllerAnimated:YES];
}
TL;DR
I made a category on UIViewController that hopefully fixes this issue for you. I can't actually reproduce the navigation bar corruption on a device, but I can do it on the simulator pretty frequently, and this category solves the problem for me. Hopefully it also solves it for you on the device.
The Problem, and the Solution
I actually don't know exactly what causes this, but the navigation bar's subviews' layers' animations seem to either be executing twice or not fully completing or... something. Anyway, I found that you can simply add some animations to these subviews in order to force them back to where they should be (with the right opacity, color, etc). The trick is to use your view controller's transitionCoordinator object and hook into a couple of events – namely the event that happens when you lift your finger up and the interactive pop gesture recognizer finishes and the rest of the animation starts, and then the event that occurs when the non-interactive half of the animation finishes.
You can hook into these events using a couple methods on the transitionCoordinator, specifically notifyWhenInteractionEndsUsingBlock: and animateAlongsideTransition:completion:. In the former, we create copies of all of the current animations of the navbar's subviews' layers, modify them slightly, and save them so we can apply them later when the non-interactive portion of the animation finishes, which is in the completion block of the latter of those two methods.
Summary
Listen for when the interactive portion of the transition ends
Gather up the animations for all the views' layers in the navigation bar
Copy and modify these animations slightly (set fromValue to the same thing as the toValue, set duration to zero, and a few other things)
Listen for when the non-interactive portion of the transition ends
Apply the copied/modified animations back to the views' layers
Code
And here's the code for the UIViewController category:
#interface UIViewController (FixNavigationBarCorruption)
- (void)fixNavigationBarCorruption;
#end
#implementation UIViewController (FixNavigationBarCorruption)
/**
* Fixes a problem where the navigation bar sometimes becomes corrupt
* when transitioning using an interactive transition.
*
* Call this method in your view controller's viewWillAppear: method
*/
- (void)fixNavigationBarCorruption
{
// Get our transition coordinator
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
// If we have a transition coordinator and it was initially interactive when it started,
// we can attempt to fix the issue with the nav bar corruption.
if ([coordinator initiallyInteractive]) {
// Use a map table so we can map from each view to its animations
NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory
valueOptions:NSMapTableStrongMemory
capacity:0];
// This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer
[coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Loop through our nav controller's nav bar's subviews
for (UIView *view in self.navigationController.navigationBar.subviews) {
NSArray *animationKeys = view.layer.animationKeys;
NSMutableArray *anims = [NSMutableArray array];
// Gather this view's animations
for (NSString *animationKey in animationKeys) {
CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey];
// In case any other kind of animation somehow gets added to this view, don't bother with it
if ([anim isKindOfClass:[CABasicAnimation class]]) {
// Make a pseudo-hard copy of each animation.
// We have to make a copy because we cannot modify an existing animation.
CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath];
// CABasicAnimation properties
// Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value
animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.byValue = anim.byValue;
// CAPropertyAnimation properties
animCopy.additive = anim.additive;
animCopy.cumulative = anim.cumulative;
animCopy.valueFunction = anim.valueFunction;
// CAAnimation properties
animCopy.timingFunction = anim.timingFunction;
animCopy.delegate = anim.delegate;
animCopy.removedOnCompletion = anim.removedOnCompletion;
// CAMediaTiming properties
animCopy.speed = anim.speed;
animCopy.repeatCount = anim.repeatCount;
animCopy.repeatDuration = anim.repeatDuration;
animCopy.autoreverses = anim.autoreverses;
animCopy.fillMode = anim.fillMode;
// We want our new animations to be instantaneous, so set the duration to zero.
// Also set both the begin time and time offset to 0.
animCopy.duration = 0;
animCopy.beginTime = 0;
animCopy.timeOffset = 0;
[anims addObject:animCopy];
}
}
// Associate the gathered animations with each respective view
[mapTable setObject:anims forKey:view];
}
}];
// The completion block here gets run after the view controller transition animation completes (or fails)
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Iterate over the mapTable's keys (views)
for (UIView *view in mapTable.keyEnumerator) {
// Get the modified animations for this view that we made when the interactive portion of the transition finished
NSArray *anims = [mapTable objectForKey:view];
// ... and add them back to the view's layer
for (CABasicAnimation *anim in anims) {
[view.layer addAnimation:anim forKey:anim.keyPath];
}
}
}];
}
}
#end
And then just call this method in your view controller's viewWillAppear: method (in your test project's case, it would be the ViewController class):
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self fixNavigationBarCorruption];
}
After investigating this issue for some time with debug console, Instruments and Reveal, I have found out the following:
1) On simulator the bug can be recreated every time, if using Profile/Automation Template and adding the following script:
var target = UIATarget.localTarget();
var appWindow = target.frontMostApp().mainWindow();
appWindow.buttons()[0].tap();
target.delay(1);
target.flickFromTo({x:2, y: 100}, {x:160, y: 100});
2) On real device (iPhone 5s, iOS 7.1) this script never causes the bug. I tried various options for flick coordinates and the delay.
3) UINavigationBar consists of:
_UINavigationBarBackground (doesn't seem to be related to the bug)
_UIBackdropView
_UIBackgropEffectView
UIView
UIImageView
UINavigationItemView
UILabel (visible in the bug)
_UINavigationBarBackIndicatorView (visible in the bug)
4) When bug happens UILabel looks half transparent and in the wrong position, but the actual properties of the UILabel are correct (alpha: 1 and frame as in normal situation). Also _UINavigationBarBackIndicatorView looks doesn't correspond to actual properties - it is visible although it's alpha is 0.
From this I conclude that it's a bug of Simulator and that you can't even detect from the code that something is wrong.
So #troop231 - are you 100% sure this also happens on device?
Key Concept
Disable gesture recognizer when pushing view controller, and enable it when view appeared.
A Common Solution: Subclassing
You can subclass UINavigationController and UIViewController to prevent corruption.
MyNavigationController : UINavigationController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[super pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO; // disable
}
MyViewController : UIViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES; // enable
}
Problem: Too annoying
Need to use MyNavigationController and MyViewController instead of UINavigationController and UIViewController.
Need to subclass for UITableViewController, UICollectionViewController, and more.
A Better Solution: Method Swizzling
It could be done by swizzling UINavigationController and UIViewController methods. Want to know about method swizzling, visit here.
Example below uses JRSwizzle that makes method swizzling easy.
UINavigationController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidLoad)
withMethod:#selector(hack_viewDidLoad)
error:nil];
[self jr_swizzleMethod:#selector(pushViewController:animated:)
withMethod:#selector(hack_pushViewController:animated:)
error:nil];
});
}
- (void)hack_viewDidLoad
{
[self hack_viewDidLoad];
self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
}
- (void)hack_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self hack_pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO;
}
UIViewController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidAppear:)
withMethod:#selector(hack_viewDidAppear:)
error:nil];
});
}
- (void)hack_viewDidAppear:(BOOL)animated
{
[self hack_viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
Being Simple: Use Open Source
SwipeBack
SwipeBack does it automatically without any code.
With CocoaPods, just add a line below into your Podfile. You don't need to write any code. CocoaPods automatically import SwipeBack globally.
pod 'SwipeBack'
Install pod, and it's done!
I have animation layers stored within SpriteBuilder. I am calling it on a touch began method
heroCharacter.m
#implementation heroCharacter{
CCNode *_heroNode;
staminaNode *_staminaReference;
}
- (void)touchBegan:(UITouch *)touches withEvent:(UIEvent *)event
//Play animation
CCBAnimationManager* animationManager = _heroNode.userObject;
[animationManager runAnimationsForSequenceNamed:#"ouch"];
}
This works fine.
I am then trying to call the animation in a custom method in another file but it then doesn't work. I have no idea why? I have tested to make sure the method is called and it is. but the actual animation isn't called. It is the same code as used in the touchBegan
-(void)sleepingHero {
//Play animation
CCBAnimationManager* animationManager = _heroNode.userObject;
[animationManager runAnimationsForSequenceNamed:#"ouch"];
No Idea how to debug this.
My custom method is being called like this in another file. Called bedroomScene.:
#implementation .....{
heroCharacter *heroHolder;
}
then in didload:
heroHolder = [[heroCharacter alloc] init];
then in another method:
[heroHolder sleepingHero];
By executing:
heroHolder = [[heroCharacter alloc] init];
in your other file, you are instantiating a new node that has no relationship whatsoever with the original _heroNode node in your first class.
What you should do is passing the actual _heroNode from your first class to your second class, so that you can reference it in the latter. There are multiple ways of doing it:
you could define a property in your second class and set it from the first (supposing the first class instantiates the second);
you could add an argument to your second class' initialization method and pass _heroNode in it;
or, you could expose _heroNode in your first class through a property (or custom accessor method) and then pass a reference to the first class into your second class.
Hope this helps.
EDIT:
you could try something like this (where you currently create your bedroomScene):
bedroomScene = [BedRoomScene scene];
bedroomScene.heroHolder = _heroNode;
For this you will need to make your heroHolder ivar into a property:
#property (nonatomic, weak) CCNode* heroHolder;
(in your BedRoomScene interface or class extension).
When is sleepingHero being called? If it is called before didLoadFromCCB is called it means that the code connections are not set up yet and you cannot reference anything created in SpriteBuilder.
I am trying to make my pause screen in my game. I am using the framework Cocos2d V3 RC4 in IOS and XCODE and SpriteBuilder. I read a lot of post and i think that i have two aproachs posible:
1º Push a total scene foward the main scene. (THIS WORK FINE TO ME)
In the MAIN SCENE i call this to pause the game
CCScene *pausa = [CCBReader loadAsScene:#"Pausa"];
[[CCDirector sharedDirector] pushScene:pausa];
and then in the Pause class i call this to pop the pause scene and take back the Main scene:
[[CCDirector sharedDirector] popScene];
2º Take a CNode in front of the MAIN SCENE, making it with transparency, opaque, and disableing the main scene touch, animations, actions, etc… (THIS DOESN’t WORK FOR ME AND I WHANT THIS !!!)
I doit in this way:
In the main Scene:
CCScene *pausa = [CCBReader loadAsScene:#"Pausa"];
[self addChild:pausa];
AND I TRY with ALL THIS METHODS:
// [self unscheduleAllSelectors];
// [self stopAllActions];
// [self setPaused:TRUE];
// [self setUserInteractionEnabled:FALSE];
The node is added but Have not Touch exclusively… The Node that is behind I can touch it…
I try olso with :
[[CCDirector sharedDirector] pushScene:pausa];
(in the main scene) with result obviosly bad, and i try with
[self setExclusiveTouch:TRUE];
in the pause didLoadFromCCB method but also I cant make it have a Exclusive touch. i Can STILLPRESS buttons and sprites from the back Node…
What I am doing Wrong, And how is the correct code/aproach tu use to handle a pause node like I want for method 2??
Resuming... I only want a Modal Window... (like in zk framework, in java, the Window (CNode in Cocos2d) come in front and the background keep disabled and in grey)
Thanks for read and hope someone can help
Here is my implementation from a game that I am doing
- (void) pauseGame
{
CCLOG(#"Pause game");
_contentNode.paused = YES;
_contentNode.userInteractionEnabled = NO;
_gamePausedNode = (GamePausedNode *)[self loadCCBWithNameAndPositionInCenter:#"PausedNode"];
[self addChild:_gamePausedNode];
}
gamePausedGame is a CCNode, but it could be CCSprite as well. It is not actually a CCScene, nor is it loaded by one because a modal view like this is not really a scene.
You usually want to group the CCNode objects together in one CCNode like my _contentNode so you can pause them with one click.
Update : I have edited the code to the bare minimals
CCPhysicsNode *_physics;
_physics.paused = true;// It will pause your game but not actions.
_physics.paused = false;// It will resume your spinning and falling of sprites while button is pressable like during pause game.
I create a GLKViewController like this:
// Create a GLK View Controller to handle animation timings
_glkVC = [[GLKViewController alloc] initWithNibName:nil bundle:nil];
_glkVC.preferredFramesPerSecond = 60;
_glkVC.view = self.glkView;
_glkVC.delegate = self;
_glkVC.paused = YES;
NSLog(#"initial state: %#", _glkVC.paused ? #"paused" : #"running");
but it immediately starts calling the delegate update method and the output from the NSLog above is: initial state: running
I am managing my view updates with setNeedsDisplay but I want the GLKViewController to handle animations from time to time so I want to unpause it only when needed. Is there a way to start the controller in a paused state?
have you tried pausing in the viewDidAppear method instead of the viewDidLoad method? It should look something like this:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// self.paused automatically set to NO in super's implementation
self.paused = YES;
}
Boom, done! If this works then you save an "if" check thousands of times a minute just to pause on launch!
The viewDidAppear method works for me, but not optimally. A few frames of visible animation occur before the pause takes effect. Using viewWillAppear worked much better:
- (void) viewWillAppear: (BOOL) animated
{
[ super viewDidAppear: animated ];
self.paused = YES;
}
In lieu of any answers I'm using this work-around:
I set .preferredFramesPerSecond = 1 initially and then in the update method I check if(preferredFramesPerSecond == 1) and set .paused = YES (and also set my real desired value for preferredFramesPerSecond). I can then allow the rest of the update method to run once after initialisation, or return immediately if I don't want it to run yet.
I then trigger redraws manually as needed with setNeedsDisplay and unpause it when I need it to animate.
If anyone has a better solution please answer as usual.
Have you tried overriding resumeOnDidBecomeActive to return NO? This should keep the animation paused on any activation, including the first.