Swallow touches, unless touching a child of my current layer - iphone

I am writing a pause menu using a CCLayer. I need the layer to swallow touches so that you cannot press the layer below, however I also need to be able to use the buttons on the pause layer itself.
I can get the layer to swallow touches, but the menu won't work either.
Here is my code:
pauseLayer.m
#import "PauseLayer.h"
#implementation PauseLayer
#synthesize delegate;
+ (id) layerWithColor:(ccColor4B)color delegate:(id)_delegate
{
return [[[self alloc] initWithColor:color delegate:_delegate] autorelease];
}
- (id) initWithColor:(ccColor4B)c delegate:(id)_delegate {
self = [super initWithColor:c];
if (self != nil) {
NSLog(#"Init");
self.isTouchEnabled = YES;
CGSize wins = [[CCDirector sharedDirector] winSize];
delegate = _delegate;
[self pauseDelegate];
CCSprite * background = [CCSprite spriteWithFile:#"pause_background.png"];
[self addChild:background];
CCMenuItemImage *resume = [CCMenuItemImage itemFromNormalImage:#"back_normal.png"
selectedImage:#"back_clicked.png"
target:self
selector:#selector(doResume:)];
resume.tag = 10;
CCMenu * menu = [CCMenu menuWithItems:resume,nil];
[menu setPosition:ccp(0,0)];
[resume setPosition:ccp([background boundingBox].size.width/2,[background boundingBox].size.height/2)];
[background addChild:menu];
[background setPosition:ccp(wins.width/2,wins.height/2)];
}
return self;
}
-(void)pauseDelegate
{
NSLog(#"pause delegate");
if([delegate respondsToSelector:#selector(pauseLayerDidPause)])
[delegate pauseLayerDidPause];
}
-(void)doResume: (id)sender
{
if([delegate respondsToSelector:#selector(pauseLayerDidUnpause)])
[delegate pauseLayerDidUnpause];
[self.parent removeChild:self cleanup:YES];
}
- (void)onEnter {
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:INT_MIN+1 swallowsTouches:YES];
[super onEnter];
}
- (void)onExit {
[[CCTouchDispatcher sharedDispatcher] removeDelegate:self];
[super onExit];
}
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
return YES;
}
-(void)dealloc
{
[super dealloc];
}
#end

why dont you just disable touches on the game layer?
like in the onEnter method disable the touches on the game layer..and onExit re enable them
something like
-onEnter{
gameLayer.isTouchEnabled=NO;
....
}
-onExit{
gameLater.isTouchEnabled=YES;
...
}
also you wont need CCTouchDispatcher

According to your code, the problem is the modal layer is swallowing events even if it's for the own children.
To solve this problem, you have to set the touch priority of the children even higher than the modal layer itself.
In other words, set the menu's touch priority value below modal layer's.
There are two solutions.
Simply override "CCMenu::registerWithTouchDispatcher()" method and set the priority higher from the beginning.
Change menu's touch priority using "setPriority" method of the touchDispatcher or "setHandlerPriority" of menu itself.
To achieve second solution, you have to pay attention to the timing.
"CCMenu::registerWithTouchDispatcher()" is called somewhere AFTER "onEnter" and "onEnterTransitionDidFinish".
So, use "scheduleOnce" or something like that.
Sample codes.
- (id) initWithColor:(ccColor4B)c delegate:(id)_delegate {
self = [super initWithColor:c];
if (self != nil) {
//your codes...... put CCMenu in instance
[self scheduleOnce:#selector(setMenuPriority:) delay:0];
}
return self;
}
- (void) setMenuPriority (ccTime)dt {
[[[CCDirector sharedDirector] touchDispatcher] setPriority:INT_MIN forDelegate:menu];
//priority "INT_MIN" because you set the layer's priority to "INT_MIN+1".
}
PS: My english is not so good, so if there are loose sentences, correction will be very pleased :)

The problem is, that the layer/node hierarchy is not considered when propagating touches.
The touches are handed from the touch handler with the smallest priority value to the ones with the highest.
The touches are not forwarded anymore, once one of the responders swallows the touch.
You can use an approach similar to CCMenu. CCMenu handles all touches and detects which CCMenuItemhas been selected, based on the position of these items.
If you implement this the same way, you let your PauseLayer handle and swallow all touches and use a seperate mechanism to determine which child element in your PauseLayer has been selected.
Example Implementation: CCMenu Subclass
I have implemented a solution in this repository:
https://github.com/Ben-G/MGWU-Widgets/tree/master/Projectfiles/Components/CCMenuBlocking
That component is a CCMenuSubclass that swallows all touches and does not forward them.
Example Implementation: CCNode
Here is a more general solution of a CCNode that swallows touches and only forwards them to its own children:
https://github.com/Ben-G/MGWU-Widgets/blob/master/Projectfiles/Components/Popup/PopUp.m

Related

Why does UINavigationBar steal touch events?

I have a custom UIButton with UILabel added as subview. Button perform given selector only when I touch it about 15points lower of top bound. And when I tap above that area nothing happens.
I found out that it hasn't caused by wrong creation of button and label, because after I shift the button lower at about 15 px it works correctly.
UPDATE I forgot to say that button located under the UINavigationBar and 1/3 of upper part of the button don't get touch events.
Image was here
View with 4 buttons is located under the NavigationBar. And when touch the "Basketball" in top, BackButton get touch event, and when touch "Piano" in top, then rightBarButton (if exists) get touch. If not exists, nothing happened.
I didn't find this documented feature in App docs.
Also I found this topic related to my problem, but there is no answer too.
I noticed that if you set userInteractionEnabled to OFF, the NavigationBar doesn't "steal" the touches anymore.
So you have to subclass your UINavigationBar and in your CustomNavigationBar do this:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
Info about how to subclass UINavigationBar you can find here.
I found out the answer here(Apple Developer Forum).
Keith at Apple Developer Technical Support, on 18th May 2010 (iPhone OS 3):
I recommend that you avoid having touch-sensitive UI in such close proximity to the nav bar or toolbar. These areas are typically known as "slop factors" making it easier for users to perform touch events on buttons without the difficulty of performing precision touches. This is also the case for UIButtons for example.
But if you want to capture the touch event before the navigation bar or toolbar receives it, you can subclass UIWindow and override:
-(void)sendEvent:(UIEvent *)event;
Also I found out,that when I touch the area under the UINavigationBar, the location.y defined as 64,though it was not.
So I made this:
CustomWindow.h
#interface CustomWindow: UIWindow
#end
CustomWindow.m
#implementation CustomWindow
- (void) sendEvent:(UIEvent *)event
{
BOOL flag = YES;
switch ([event type])
{
case UIEventTypeTouches:
//[self catchUIEventTypeTouches: event]; perform if you need to do something with event
for (UITouch *touch in [event allTouches]) {
if ([touch phase] == UITouchPhaseBegan) {
for (int i=0; i<[self.subviews count]; i++) {
//GET THE FINGER LOCATION ON THE SCREEN
CGPoint location = [touch locationInView:[self.subviews objectAtIndex:i]];
//REPORT THE TOUCH
NSLog(#"[%#] touchesBegan (%i,%i)", [[self.subviews objectAtIndex:i] class],(NSInteger) location.x, (NSInteger) location.y);
if (((NSInteger)location.y) == 64) {
flag = NO;
}
}
}
}
break;
default:
break;
}
if(!flag) return; //to do nothing
/*IMPORTANT*/[super sendEvent:(UIEvent *)event];/*IMPORTANT*/
}
#end
In AppDelegate class I use CustomWindow instead of UIWindow.
Now when I touch area under navigation bar, nothing happens.
My buttons still don't get touch events,because I don't know how to send this event (and change coordinates) to my view with buttons.
Subclass UINavigationBar and add this method. It will cause taps to be passed through unless they are tapping a subview (such as a button).
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
return v == self? nil: v;
}
The solution for me was the following one:
First:
Add in your application (It doesn't matter where you enter this code) an extension for UINavigationBar like so:
The following code just dispatch a notification with the point and event when the navigationBar is being tapped.
extension UINavigationBar {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil, userInfo: ["point": point, "event": event as Any])
return super.hitTest(point, with: event)
}
}
Then in your specific view controller you must listen to this notification by adding this line in your viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(tapNavigationBar), name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil)
Then you need to create the method tapNavigationBar in your view controller as so:
func tapNavigationBar(notification: Notification) {
let pointOpt = notification.userInfo?["point"] as? CGPoint
let eventOpt = notification.userInfo?["event"] as? UIEvent?
guard let point = pointOpt, let event = eventOpt else { return }
let convertedPoint = YOUR_VIEW_BEHIND_THE_NAVBAR.convert(point, from: self.navigationController?.navigationBar)
if YOUR_VIEW_BEHIND_THE_NAVBAR.point(inside: convertedPoint, with: event) {
//Dispatch whatever you wanted at the first place.
}
}
PD: Don't forget to remove the observation in the deinit like so:
deinit {
NotificationCenter.default.removeObserver(self)
}
That's it... That's a little bit 'tricky', but it's a good workaround for not subclassing and getting a notification anytime the navigationBar is being tapped.
I just wanted to share another prospective to solving this problem. This is not a problem by design, but it was meant to help user get back or navigate. But we need to put things tightly in or below nav bar and things look sad.
First lets look at the code.
class MyNavigationBar: UINavigationBar {
private var secondTap = false
private var firstTapPoint = CGPointZero
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
if !self.secondTap{
self.firstTapPoint = point
}
defer{
self.secondTap = !self.secondTap
}
return super.pointInside(firstTapPoint, withEvent: event)
}
}
You might be see why am i doing second touch handling. There is the recipe to the solution.
Hit test is called twice for a call. The first time the actual point on the window is reported. Everything goes well. On the second pass, this happens.
If system sees a nav bar and the hit point is around 9 pixels more on Y side, it tries to decrease that gradually to below 44 points which is where the nav bar is.
Take a look at the screen to be clear.
So theres a mechanism that will use nearby logic to the second pass of hittest. If we can know its second pass and then call the super with first hit test point. Job done.
The above code does that exactly.
There are 2 things that might be causing problems.
Did you try setUserInteractionEnabled:NO for the label.
Second thing i think might work is apart from that after adding label on top of button you can send the label to back (it might work, not sure although)
[button sendSubviewToBack:label];
Please let me know if the code works :)
Your labels are huge. They start at {0,0} (the left top corner of the button), extend over the entire width of the button and have a height of the entire view. Check your frame data and try again.
Also, you have the option of using the UIButton property titleLabel. Maybe you are setting the title later and it goes into this label rather than your own UILabel. That would explain why the text (belonging to the button) would work, while the label would be covering the rest of the button (not letting the taps go through).
titleLabel is a read-only property, but you can customize it just as your own label (except perhaps the frame) including text color, font, shadow, etc.
This solved my problem..
I added hitTest:withEvent: code to my navbar subclass..
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
int errorMargin = 5;// space left to decrease the click event area
CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height);
BOOL isTouchAllowed = (CGRectContainsPoint(smallerFrame, point) == 1);
if (isTouchAllowed) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
Extending Alexander's solution:
Step 1. Subclass UIWindow
#interface ChunyuWindow : UIWindow {
NSMutableArray * _views;
#private
UIView *_touchView;
}
- (void)addViewForTouchPriority:(UIView*)view;
- (void)removeViewForTouchPriority:(UIView*)view;
#end
// .m File
// #import "ChunyuWindow.h"
#implementation ChunyuWindow
- (void) dealloc {
TT_RELEASE_SAFELY(_views);
[super dealloc];
}
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (UIEventSubtypeMotionShake == motion
&& [TTNavigator navigator].supportsShakeToReload) {
// If you're going to use a custom navigator implementation, you need to ensure that you
// implement the reload method. If you're inheriting from TTNavigator, then you're fine.
TTDASSERT([[TTNavigator navigator] respondsToSelector:#selector(reload)]);
[(TTNavigator*)[TTNavigator navigator] reload];
}
}
- (void)addViewForTouchPriority:(UIView*)view {
if ( !_views ) {
_views = [[NSMutableArray alloc] init];
}
if (![_views containsObject: view]) {
[_views addObject:view];
}
}
- (void)removeViewForTouchPriority:(UIView*)view {
if ( !_views ) {
return;
}
if ([_views containsObject: view]) {
[_views removeObject:view];
}
}
- (void)sendEvent:(UIEvent *)event {
if ( !_views || _views.count == 0 ) {
[super sendEvent:event];
return;
}
UITouch *touch = [[event allTouches] anyObject];
switch (touch.phase) {
case UITouchPhaseBegan: {
for ( UIView *view in _views ) {
if ( CGRectContainsPoint(view.frame, [touch locationInView:[view superview]]) ) {
_touchView = view;
[_touchView touchesBegan:[event allTouches] withEvent:event];
return;
}
}
break;
}
case UITouchPhaseMoved: {
if ( _touchView ) {
[_touchView touchesMoved:[event allTouches] withEvent:event];
return;
}
break;
}
case UITouchPhaseCancelled: {
if ( _touchView ) {
[_touchView touchesCancelled:[event allTouches] withEvent:event];
_touchView = nil;
return;
}
break;
}
case UITouchPhaseEnded: {
if ( _touchView ) {
[_touchView touchesEnded:[event allTouches] withEvent:event];
_touchView = nil;
return;
}
break;
}
default: {
break;
}
}
[super sendEvent:event];
}
#end
Step 2: Assign ChunyuWindow instance to AppDelegate Instance
Step 3: Implement touchesEnded:widthEvent: for view with buttons, for example:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded: touches withEvent: event];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: _buttonsView]; // a subview contains buttons
for (UIButton* button in _buttons) {
if (CGRectContainsPoint(button.frame, point)) {
[self onTabButtonClicked: button];
break;
}
}
}
Step 4: call ChunyuWindow's addViewForTouchPriority when the view we care about appears, and call removeViewForTouchPriority when the view disappears or dealloc, in viewDidAppear/viewDidDisappear/dealloc of ViewControllers, so _touchView in ChunyuWindow is NULL, and it is the same as UIWindow, having no side effects.
An alternative solution that worked for me, based on the answer provided by Alexandar:
self.navigationController?.barHideOnTapGestureRecognizer.enabled = false
Instead of overriding the UIWindow, you can just disable the gesture recogniser responsible for the "slop area" on the UINavigationBar.
Give a extension version according to Bart Whiteley. No need to subclass.
#implementation UINavigationBar(Xxxxxx)
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
return v == self ? nil: v;
}
#end
The following worked for me:
self.navigationController?.isNavigationBarHidden = true

iPhone, Cocos2D - moving sprite left/right while touching screen

I'm new to Objective C and app development so please go easy on me!
I'm trying to make a basic game and need to move a sprite left or right continuously while the user's finger is on the screen - left side to go left, right to go right...
I'm trying to use update to repeat movements of a few pixels every 1/60th second. So far, this is what I have (and sorry about the formatting):
#import "GameplayLayer.h"
#implementation GameplayLayer
-(id)init {
self = [super init];
if (self != nil) {
CGSize screenSize = [CCDirector sharedDirector].winSize;
// enable touches
self.isTouchEnabled = YES;
blobSprite = [CCSprite spriteWithFile:#"blob.png"];
[blobSprite setPosition: CGPointMake(screenSize.width/2, screenSize.height*0.17f)];
ball = [CCSprite spriteWithFile:#"ball.png"];
[ball setPosition:CGPointMake(10, screenSize.height*0.75f)];
[self addChild:blobSprite];
[self addChild:ball];
[self schedule:#selector(update) interval:1.0f/60.0f];
}
return self;
}
-(void) update:(ccTime)dt{
if (_tapDownLeft == YES){
blobSprite.position.x==blobSprite.position.x-5;
}
if (_tapDownRight == YES){
blobSprite.position.x==blobSprite.position.x+5;
}
}
-(void) ccTouchesBegan:(UITouch*)touch withEvent: (UIEvent *)event{
CGPoint touchLocation = [touch locationInView:[touch view]];
touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
if (touchLocation.x > 400) {
if ((blobSprite.position.x+10)<460){
_tapDownRight = YES;
}
}
if (touchLocation.x < 200) {
if ((blobSprite.position.x-10>20)){
_tapDownLeft = YES;
}
}
else {
_tapDownLeft = NO;
_tapDownRight = NO;
}
}
-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event{
_tapDownLeft = NO;
_tapDownRight = NO;
}
-(void) registerWithTouchDispatcher{
[[CCTouchDispatcher sharedDispatcher]addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
#end
Am I on the right lines with this? At the moment it's giving me 'expression result unused' in update. Could anyone tell me what I'm missing? Any help would be greatly appreciated.
Thanks,
Patrick
i see a few things here:
not certain your selector will call update : #selector(update:)
I would not rely on dt being either exactly 1/60th of a second, nor being constant. I would favor defining a speed constant (in points per second) and compute the deltaX in points based on the desired speed and dt, at each update cycle.
I dont see a 'registerWithTouchDispatcher' call (i try to place them in onEnter and onExit) methods.
Somewhere in there, make certain you remove your children (either in dealloc, or better in a local cleanup method (dont forget to invoke [super cleanup]).
Remove the argument in the update function

cocos2d-iOS - Gesture recognisers

Has anyone managed to get the gesture recognition working in cocos-2d?
I have read a post here that claimed to have achieved it, here: http://www.cocos2d-iphone.org/forum/topic/8929
I patched from the git hub here: https://github.com/xemus/cocos2d-GestureRecognizers/blob/master/README
I made a subclass of CCSprite (which is a subclass of CCNode):
-(id) initWithTexture:(CCTexture2D*)texture rect:(CGRect)rect {
if( (self=[super initWithTexture:texture rect:rect]) )
{
CCGestureRecognizer* recognizer;
recognizer = [CCGestureRecognizer
CCRecognizerWithRecognizerTargetAction:[[[UITapGestureRecognizer alloc]init] autorelease]
target:self
action:#selector(tap:node:)];
[self addGestureRecognizer:recognizer];
}
return self;
}
Delegate method:
- (void) swipe:(UIGestureRecognizer*)recognizer node:(CCNode*)node
{
NSLog(#" I never get called :( ");
}
My tap event never gets called.
Has anyone got this working? How difficult is it to do gesture recognition manually for swipe detection?
You need to attach the gesture recognizer to something "up the chain". Don't attach them to the individual nodes; attach them to the UIView (i.e., [[CCDirector sharedDirector] openGLView]).
Here's what I did:
- (UIPanGestureRecognizer *)watchForPan:(SEL)selector number:(int)tapsRequired {
UIPanGestureRecognizer *recognizer = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:selector] autorelease];
recognizer.minimumNumberOfTouches = tapsRequired;
[[[CCDirector sharedDirector] openGLView] addGestureRecognizer:recognizer];
return recognizer;
}
- (void)unwatch:(UIGestureRecognizer *)gr {
[[[CCDirector sharedDirector] openGLView] removeGestureRecognizer:gr];
}
This particular code is used in a superclass for scene controllers, so the target for the selector is hard-coded to "self", but you could easily abstract that to a passed-in object. Also, you could extrapolate the above to easily create gesture recognizers for taps, pinches, etc.
In the subclass for the controller, then, I just do this:
- (MyController *)init {
if ((self = [super init])) {
[self watchForPan:#selector(panning:) number:1];
}
return self;
}
- (void)panning:(UIPanGestureRecognizer *)recognizer {
CGPoint p;
CGPoint v;
switch( recognizer.state ) {
case UIGestureRecognizerStatePossible:
case UIGestureRecognizerStateBegan:
p = [recognizer locationInView:[CCDirector sharedDirector].openGLView];
(do something when the pan begins)
break;
case UIGestureRecognizerStateChanged:
p = [recognizer locationInView:[CCDirector sharedDirector].openGLView];
(do something while the pan is in progress)
break;
case UIGestureRecognizerStateFailed:
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
(do something when the pan ends)
(the below gets the velocity; good for letting player "fling" things)
v = [recognizer velocityInView:[CCDirector sharedDirector].openGLView];
break;
}
}
If you don't want to handle everything manually I created a simple category that will add gesture recognizers to any cocos2d version
read more at:
http://www.merowing.info/2012/03/using-gesturerecognizers-in-cocos2d/
or grab it from github
https://github.com/krzysztofzablocki/CCNode-SFGestureRecognizers

CCMenuItemImage not responding to touches!

So I am adding a CCMenuItemImage to my layer like so:
CCMenuItemImage *pauseButton = [CCMenuItemImage itemFromNormalImage:#"pausebutton.png"
selectedImage:#"pausebutton.png" // TODO add selected image
disabledImage:#"pausebutton.png"
target:self
selector:#selector(pauseGame:)];
pauseButton.position = ccp(24, 292);
[self addChild:pauseButton];
The problem is my pauseGame: selector is never triggered when I touch the pause button!
I have verified that the selector is set properly by doing a [pauseButton activate] (calls the selector).
Also, I have verified that my layer is responding to touches by outputting logging information in ccTouchesBegan and ccTouchesEnded.
It is also worth noting that I have sprites in my layer which register themselves for touches like so:
- (void) onEnter
{
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
[super onEnter];
}
What could the problem be?
Hmm... You didnt add to a CCMenu...
CCMenu* menu = [CCMenu menuWithItems:pauseButton, nil];
menu.isTouchEnabled = YES;
[self addChild:menu];
Note that your pausegame should be:
-(void)pauseGame:(id)sender
{
//pause game!!!
}

action won't run on a CCSprite from within ccTouchBegan in cocos2d for iphone

I'm trying to basically scale up a button as soon as a touch is detected. Here's my Scene:
#implementation HomeScene
-(id) init
{
if((self = [super init])) {
...
// sp_btn_story is retained...
sp_btn_story = [[CCSprite spriteWithFile:#"main_menu_btn.png"] retain];
sp_btn_story.position = ccp(size.width - 146, 110);
[self addChild: sp_btn_story];
...
}
return self;
}
-(void) onEnter
{
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
-(void) onExit
{
[[CCTouchDispatcher sharedDispatcher] removeDelegate:self];
}
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
NSLog(#"tapped!");
sp_btn_story.scaleX = 2;
[sp_btn_story stopAllActions];
[sp_btn_story runAction: [CCScaleTo actionWithDuration:0.5f scale:1.2f]];
return YES;
}
...
#end
It scales the X just fine, as expected. (I threw that in there to test.) But the action is not running for some reason. :( Anyone have any ideas?
Edit: using cocos2d 0.99 btw.
The answer for this question (reposted from the asker's own comment to make it more visible):
It's quite important to call
[super onEnter];
if you overwrite this method in a subclass or strange things may happen. Cocos2D will not be happy.
This applies to other (if not all) methods as well, e. g. always call
[super onExit];
as well.
Not complete sure what could be going on. Looks like what you have should work fine but you may want to try applying a different action to sp_btn_story like fade in or fade out to see if at least any action will work. Failing that you could also try applying the action in a different part of your code. These are not solutions but they may provide evidence to indicate what exactly is going on.