By AlexHeuman


2013-09-29 19:18:27 8 Comments

I'm discovering that UIButtons don't work very well with SKScene, So I'm attempting to subclass SKNode to make a button in SpriteKit.

The way I would like it to work is that if I initialize a button in SKScene and enable touch events, then the button will call a method in my SKScene when it is pressed.

I'd appreciate any advice that would lead me to finding the solution to this problem. Thanks.

17 comments

@dennis-tra 2013-10-05 16:03:31

I've made my own Button-Class that I'm working with. SKButton.h:

#import <SpriteKit/SpriteKit.h>
@interface SKButton : SKSpriteNode

@property (nonatomic, readonly) SEL actionTouchUpInside;
@property (nonatomic, readonly) SEL actionTouchDown;
@property (nonatomic, readonly) SEL actionTouchUp;
@property (nonatomic, readonly, weak) id targetTouchUpInside;
@property (nonatomic, readonly, weak) id targetTouchDown;
@property (nonatomic, readonly, weak) id targetTouchUp;

@property (nonatomic) BOOL isEnabled;
@property (nonatomic) BOOL isSelected;
@property (nonatomic, readonly, strong) SKLabelNode *title;
@property (nonatomic, readwrite, strong) SKTexture *normalTexture;
@property (nonatomic, readwrite, strong) SKTexture *selectedTexture;
@property (nonatomic, readwrite, strong) SKTexture *disabledTexture;

- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected;
- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected disabled:(SKTexture *)disabled; // Designated Initializer

- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected;
- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected disabled:(NSString *)disabled;

/** Sets the target-action pair, that is called when the Button is tapped.
 "target" won't be retained.
 */
- (void)setTouchUpInsideTarget:(id)target action:(SEL)action;
- (void)setTouchDownTarget:(id)target action:(SEL)action;
- (void)setTouchUpTarget:(id)target action:(SEL)action;

@end

SKButton.m:

#import "SKButton.h"
#import <objc/message.h>


@implementation SKButton

#pragma mark Texture Initializer

/**
 * Override the super-classes designated initializer, to get a properly set SKButton in every case
 */
- (id)initWithTexture:(SKTexture *)texture color:(UIColor *)color size:(CGSize)size {
    return [self initWithTextureNormal:texture selected:nil disabled:nil];
}

- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected {
    return [self initWithTextureNormal:normal selected:selected disabled:nil];
}

/**
 * This is the designated Initializer
 */
- (id)initWithTextureNormal:(SKTexture *)normal selected:(SKTexture *)selected disabled:(SKTexture *)disabled {
    self = [super initWithTexture:normal color:[UIColor whiteColor] size:normal.size];
    if (self) {
        [self setNormalTexture:normal];
        [self setSelectedTexture:selected];
        [self setDisabledTexture:disabled];
        [self setIsEnabled:YES];
        [self setIsSelected:NO];

        _title = [SKLabelNode labelNodeWithFontNamed:@"Arial"];
        [_title setVerticalAlignmentMode:SKLabelVerticalAlignmentModeCenter];
        [_title setHorizontalAlignmentMode:SKLabelHorizontalAlignmentModeCenter];

        [self addChild:_title];
        [self setUserInteractionEnabled:YES];
    }
    return self;
}

#pragma mark Image Initializer

- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected {
    return [self initWithImageNamedNormal:normal selected:selected disabled:nil];
}

- (id)initWithImageNamedNormal:(NSString *)normal selected:(NSString *)selected disabled:(NSString *)disabled {
    SKTexture *textureNormal = nil;
    if (normal) {
        textureNormal = [SKTexture textureWithImageNamed:normal];
    }

    SKTexture *textureSelected = nil;
    if (selected) {
        textureSelected = [SKTexture textureWithImageNamed:selected];
    }

    SKTexture *textureDisabled = nil;
    if (disabled) {
        textureDisabled = [SKTexture textureWithImageNamed:disabled];
    }

    return [self initWithTextureNormal:textureNormal selected:textureSelected disabled:textureDisabled];
}




#pragma -
#pragma mark Setting Target-Action pairs

- (void)setTouchUpInsideTarget:(id)target action:(SEL)action {
    _targetTouchUpInside = target;
    _actionTouchUpInside = action;
}

- (void)setTouchDownTarget:(id)target action:(SEL)action {
    _targetTouchDown = target;
    _actionTouchDown = action;
}

- (void)setTouchUpTarget:(id)target action:(SEL)action {
    _targetTouchUp = target;
    _actionTouchUp = action;
}

#pragma -
#pragma mark Setter overrides

- (void)setIsEnabled:(BOOL)isEnabled {
    _isEnabled = isEnabled;
    if ([self disabledTexture]) {
        if (!_isEnabled) {
            [self setTexture:_disabledTexture];
        } else {
            [self setTexture:_normalTexture];
        }
    }
}

- (void)setIsSelected:(BOOL)isSelected {
    _isSelected = isSelected;
    if ([self selectedTexture] && [self isEnabled]) {
        if (_isSelected) {
            [self setTexture:_selectedTexture];
        } else {
            [self setTexture:_normalTexture];
        }
    }
}

#pragma -
#pragma mark Touch Handling

/**
 * This method only occurs, if the touch was inside this node. Furthermore if 
 * the Button is enabled, the texture should change to "selectedTexture".
 */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self isEnabled]) {
        objc_msgSend(_targetTouchDown, _actionTouchDown);
        [self setIsSelected:YES];
    }
}

/**
 * If the Button is enabled: This method looks, where the touch was moved to.
 * If the touch moves outside of the button, the isSelected property is restored
 * to NO and the texture changes to "normalTexture".
 */
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self isEnabled]) {
        UITouch *touch = [touches anyObject];
        CGPoint touchPoint = [touch locationInNode:self.parent];

        if (CGRectContainsPoint(self.frame, touchPoint)) {
            [self setIsSelected:YES];
        } else {
            [self setIsSelected:NO];
        }
    }
}

/**
 * If the Button is enabled AND the touch ended in the buttons frame, the
 * selector of the target is run.
 */
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInNode:self.parent];

    if ([self isEnabled] && CGRectContainsPoint(self.frame, touchPoint)) {
        objc_msgSend(_targetTouchUpInside, _actionTouchUpInside);
    }
    [self setIsSelected:NO];
    objc_msgSend(_targetTouchUp, _actionTouchUp);
}

An example: To initialize a button, you write the following lines:

    SKButton *backButton = [[SKButton alloc] initWithImageNamedNormal:@"buttonNormal" selected:@"buttonSelected"];
    [backButton setPosition:CGPointMake(100, 100)];
    [backButton.title setText:@"Button"];
    [backButton.title setFontName:@"Chalkduster"];
    [backButton.title setFontSize:20.0];
    [backButton setTouchUpInsideTarget:self action:@selector(buttonAction)];
    [self addChild:backButton];

Furthermore you need the 'buttonAction' method in your class. * No warranty that this class is working right in every case. I'm still quite new to objective-c. *

If you think having to do this is annoying and pointless you can disable the check in the build settings by setting 'Enable strict checking of objc_msgSend Calls' to 'No'

@Jeffrey W. 2013-10-27 10:41:30

Thanks for sharing. Is there a reason why you use objc_msgSend instead of [target performSelector:selector]?

@Jeffrey W. 2013-11-04 22:58:00

Ah yes, darn ARC. I forgot about that warning :| Here's a nice work around if you're interested stackoverflow.com/questions/11895287/…

@Beau Nouvelle 2013-11-10 10:05:02

The above code is great, but I'm getting errors when trying to use - (void)changeToScene:(SKButtonNode *)sender {} as the @selector. I'd rather use a single method to switch scenes by using sender.name if I could.

@James Paul Mason 2014-01-12 23:18:40

Thanks for sharing this! I'm including it in my code. We'll see if it works. One suggestion: change the name of the class from SKButton to something more unique to you e.g., GRFButton. At some point, Apple may introduce an SKButton and you don't want to confuse the namespace and break your code later on.

@Gomfucius 2014-02-14 06:06:11

@BeauYoung - It works when you add the self at the end like so: objc_msgSend(_targetTouchUpInside, _actionTouchUpInside, self)

@Beau Nouvelle 2014-02-15 15:54:55

@Gomfucius That's amazing! Saved me heaps of trouble. Still quite new to using messages. Why would this have been left out in the first place?

@Gomfucius 2014-02-18 18:38:10

@BeauYoung - I'm not sure, that would be a question for Graf.

@user3138007 2014-05-05 17:38:53

how do i add the buttonAction method?

@chakrit 2014-07-30 04:36:18

This should be cocoapod-ified or at least put on github.

@Patrick Collins 2015-01-20 08:23:49

This code is throwing an error in xcode6 in regard to the use of objc_msgSend. See here for the solution: stackoverflow.com/questions/24922913/…

@wovencharlie 2015-01-21 15:32:41

With iOS 8, using "normalTexture" will cause a conflict. To resolve this, you will need to rename "normalTexture" to something such as "myNormalTexture". This is a great little class btw, thanks for sharing! Reference this post: stackoverflow.com/questions/26670506/…

@Luca Angeletti 2018-08-20 11:38:42

Here's a simple button written with modern Swift (4.1.2)

Features

  • it accepts 2 image names, 1 for the default state and one for the active state
  • the developer can set the touchBeganCallback and touchEndedCallback closures to add custom behaviour

Code

import SpriteKit

class SpriteKitButton: SKSpriteNode {

    private let textureDefault: SKTexture
    private let textureActive: SKTexture

    init(defaultImageNamed: String, activeImageNamed:String) {
        textureDefault = SKTexture(imageNamed: defaultImageNamed)
        textureActive = SKTexture(imageNamed: activeImageNamed)
        super.init(texture: textureDefault, color: .clear, size: textureDefault.size())
        self.isUserInteractionEnabled = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("Not implemented")
    }

    var touchBeganCallback: (() -> Void)?
    var touchEndedCallback: (() -> Void)?

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.texture = textureActive
        touchBeganCallback?()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.texture = textureDefault
        touchEndedCallback?()
    }
}

How to use it

class GameScene: SKScene {

    override func didMove(to view: SKView) {

        // 1. create the button
        let button = SpriteKitButton(defaultImageNamed: "default", activeImageNamed: "active")

        // 2. write what should happen when the button is tapped
        button.touchBeganCallback = {
            print("Touch began")
        }

        // 3. write what should happen when the button is released
        button.touchEndedCallback = {
            print("Touch ended")
        }

        // 4. add the button to the scene
        addChild(button)

    }
}

@Oleg O 2018-02-26 16:33:05

Unfortunately SpriteKit does not have button node, I do not know why, because it is very useful control. So I decided to create my own and share via CocoaPods, please use it OOButtonNode. Buttons can use text/background or images, written in Swift 4.

@Bersaelor 2017-12-07 20:35:53

I wasn't convinced of any of the above options, so based on the latest Swift4 I created my own solution.

@mogelbuster 2016-11-01 00:51:24

What a lot of great solutions to this problem! For the hardcore scrollers that make it down this far, you're in for a treat! I have subclassed SKScene, and it takes ONE function call to register ANY node to act like a UIButton! Here is the class:

class KCScene : SKScene {
//------------------------------------------------------------------------------------
//This function is the only thing you use in this class!!!
func addButton(_ node:SKNode, withCompletionHandler handler: @escaping ()->()) {
    let data = ButtonData(button: node, actionToPerform: handler)
    eligibleButtons.append(data)
}
//------------------------------------------------------------------------------------
private struct ButtonData {
    //TODO: make a dictionary with ()->() as the value and SKNode as the key.
    //Then refactor this class!
    let button:SKNode
    let actionToPerform:()->()
}

private struct TouchTrackingData {
    //this will be in a dictionary with a UITouch object as the key
    let button:SKNode
    let originalButtonFrame:CGRect
}

private var eligibleButtons = [ButtonData]()
private var trackedTouches = [UITouch:TouchTrackingData]()
//------------------------------------------------------------------------------------
//TODO: make these functions customizable,
//with these implementations as defaults.
private func applyTouchedDownEffectToNode(node:SKNode) {
    node.alpha  = 0.5
    node.xScale = 0.8
    node.yScale = 0.8
}
private func applyTouchedUpEffectToNode(node:SKNode)   {
    node.alpha  = 1
    node.xScale = 1
    node.yScale = 1
}
//------------------------------------------------------------------------------------
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    for touch in touches {
        let touchLocation = touch.location(in: self)
        let touchedNode = atPoint(touchLocation)

        for buttonData in eligibleButtons {
            if touchedNode === buttonData.button {
                //then this touch needs to be tracked, as it touched down on an eligible button!
                for (t, bD) in trackedTouches {
                    if bD.button === buttonData.button {
                        //then this button was already being tracked by a previous touch, disable the previous touch
                        trackedTouches[t] = nil
                    }
                }
                //start tracking this touch
                trackedTouches[touch] = TouchTrackingData(button: touchedNode, originalButtonFrame: touchedNode.frameInScene)
                applyTouchedDownEffectToNode(node: buttonData.button)
            }
        }
    }
}
//------------------------------------------------------------------------------------
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    for touch in touches {
        if trackedTouches[touch] == nil {continue}
        //Now we know this touch is being tracked...
        let touchLocation = touch.location(in: self)
        //TODO: implement an isBeingTouched property on TouchTrackingData, so 
        //applyTouchedDown(Up)Effect doesn't have to be called EVERY move the touch makes
        if trackedTouches[touch]!.originalButtonFrame.contains(touchLocation) {
            //if this tracked touch is touching its button
            applyTouchedDownEffectToNode(node: trackedTouches[touch]!.button)
        } else {
            applyTouchedUpEffectToNode(node: trackedTouches[touch]!.button)
        }

    }
}
//------------------------------------------------------------------------------------
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    for touch in touches {
        if trackedTouches[touch] == nil {continue}
        //Now we know this touch is being tracked...
        let touchLocation = touch.location(in: self)

        if trackedTouches[touch]!.originalButtonFrame.contains(touchLocation) {
            applyTouchedUpEffectToNode(node: trackedTouches[touch]!.button)

            for buttonData in eligibleButtons {
                if buttonData.button === trackedTouches[touch]!.button {
                    buttonData.actionToPerform()
                }
            }
        }
        trackedTouches[touch] = nil
    }
}
//------------------------------------------------------------------------------------
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
    for touch in touches! {
        if trackedTouches[touch] == nil {continue}
        //Now we know this touch is being tracked...
        //Since this touch was cancelled, it will not be activating a button,
        //and it is not worth checking where the touch was
        //we will simply apply the touched up effect regardless and remove the touch from being tracked
        applyTouchedUpEffectToNode(node: trackedTouches[touch]!.button)
        trackedTouches[touch] = nil
    }
}
//------------------------------------------------------------------------------------

}

It includes a lot of ideas I haven't yet implemented and some explanations of the code, but just copy and paste it into your project, and you can use it as-is in your own scene. Here is a complete example usage:

class GameScene : KCScene {
var playButton:SKSpriteNode
override init(size:CGSize) {
    playButton = SKSpriteNode(color: SKColor.red, size: CGSize(width:200,height:200))
    playButton.position.x = size.width/2
    playButton.position.y = size.height*0.75
    super.init(size: size)
}
override func didMove(to view: SKView) {
    addChild(playButton)
    addButton(playButton, withCompletionHandler: playButtonPushed)
}
func playButtonPushed() {
    let scene = GameScene(size: CGSize(width: 768, height: 1024))
    scene.scaleMode = .aspectFill
    view!.presentScene(scene)
}
}

The one caveat, is if you implement touchesBegan, touchesMoved, touchesEnded, and/or touchesCancelled you MUST CALL SUPER! Or else it will not work.

And please realize that in that example, there is really only ONE LINE OF CODE you need to give ANY NODE UIButton characteristics! It was this line:

addButton(playButton, withCompletionHandler: playButtonPushed)

I'm always open for ideas and suggestions. Leave 'em in the comments and Happy Coding!!

Oops, I forgot to mention I use this nifty extension. You can take it out of an extension (as you probably don't need it in every node) and plop it in my class. I only use it in one place.

extension SKNode {
var frameInScene:CGRect {
    if let scene = scene, let parent = parent {
        let rectOriginInScene = scene.convert(frame.origin, from: parent)
        return CGRect(origin: rectOriginInScene, size: frame.size)
    }
    return frame
}

}

@Confused 2016-11-20 23:32:58

How does this ensure that the playButtonPushed completion function is accessible? Or where do I put the playButtonPushed function to ensure it's reachable by the KScene instance, which I assume is the button?

@mogelbuster 2016-12-06 13:27:36

@Confused You would make your own scene a subclass of KCScene instead of SKScene: class ConfusedScene : KCScene {. Then inside ConfusedScene just make a function to do what you want when the button is pressed. I did this: func playButtonPushed() { /*do whatever happens when play button is pushed*/}. Why this works is too involved to explain here, but you can read about closures here.

@Alessandro Ornano 2016-02-24 08:43:05

Actually this work well on Swift 2.2 on Xcode 7.3

I like FTButtonNode (richy486/FTButtonNode.swift ) but it's not possible to specify another size (rather then default texture size) directly during initialization so I've added this simple method:

You must copy that under the official custom init method (similar to this) so you have another init method to use:

init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?, size:CGSize) {

        self.defaultTexture = defaultTexture
        self.selectedTexture = selectedTexture
        self.disabledTexture = disabledTexture
        self.label = SKLabelNode(fontNamed: "Helvetica");

        super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: size)
        userInteractionEnabled = true

        //Creating and adding a blank label, centered on the button
        self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Center;
        self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center;
        addChild(self.label)

        // Adding this node as an empty layer. Without it the touch functions are not being called
        // The reason for this is unknown when this was implemented...?
        let bugFixLayerNode = SKSpriteNode(texture: nil, color: UIColor.clearColor(), size: size)
        bugFixLayerNode.position = self.position
        addChild(bugFixLayerNode)

    }

Another important thing is the "selection time", I've seen that in the new devices (iPhone 6) sometime the time between touchesBegan and touchesEnded is too fast and you dont see the changes between defaultTexture and selectedTexture.

With this function:

func dispatchDelay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}

you can re-write the touchesEnded method to show correctly the texture variation:

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if (!isEnabled) {
            return
        }

        dispatchDelay(0.2) {
            self.isSelected = false
        }

        if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
            let touch: AnyObject! = touches.first
            let touchLocation = touch.locationInNode(parent!)

            if (CGRectContainsPoint(frame, touchLocation) ) {
                UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
            }

        }

        if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
            UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
        }
}

@Morgan Wesemann 2015-02-11 05:57:17

Edit: I've made a github repo for my SKButtonNode that I'll hopefully be keeping current and updating as swift evolves!

SKButtonNode


Unfortunately I cannot comment yet on Filip's swift implementation of SKButton in Swift. Super happy that he made this in Swift! But, I noticed that he didn't include a function to add text to the button. This is a huge feature to me, so that you don't have to create separate assets for every single button, rather just the background and add dynamic text.

I added a simple function to add a text label to SKButton. It likely isn't perfect--I'm new to Swift just like everyone else! Feel free to comment and help me update this to the best it can be. Hope you guys like!

 //Define label with the textures
 var defaultTexture: SKTexture
 var selectedTexture: SKTexture

 //New defining of label
 var label: SKLabelNode

 //Updated init() function:

 init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {

    self.defaultTexture = defaultTexture
    self.selectedTexture = selectedTexture
    self.disabledTexture = disabledTexture

    //New initialization of label
    self.label = SKLabelNode(fontNamed: "Helvetica");

    super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())
    userInteractionEnabled = true

    //Creating and adding a blank label, centered on the button
    self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Center;
    self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center;
    addChild(self.label)

    // Adding this node as an empty layer. Without it the touch functions are not being called
    // The reason for this is unknown when this was implemented...?
    let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
    bugFixLayerNode.position = self.position
    addChild(bugFixLayerNode)

  }




    /*
      New function for setting text. Calling function multiple times does 
      not create a ton of new labels, just updates existing label.
      You can set the title, font type and font size with this function
    */

    func setButtonLabel(#title: NSString, font: String, fontSize: CGFloat) {
        var title = title
        var font = font
        var fontSize = fontSize

        self.label.text = title
        self.label.fontSize = fontSize
        self.label.fontName = font        
     } 

Sample creation of button:

    var buttonTexture = SKTexture(imageNamed: "Button");
    var buttonPressedTexture = SKTexture(imageNamed: "Button Pressed");
    var button = SKButton(normalTexture:buttonTexture, selectedTexture:buttonPressedTexture, disabledTexture:buttonPressedTexture);
    button.setButtonLabel(title: "Play",font: "Helvetica",fontSize: 40);
    button.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
    self.addChild(button);

Full Class Listed Below:

import Foundation
import SpriteKit


class SKButton: SKSpriteNode {




enum FTButtonActionType: Int {
    case TouchUpInside = 1,
    TouchDown, TouchUp
}

var isEnabled: Bool = true {
    didSet {
        if (disabledTexture != nil) {
            texture = isEnabled ? defaultTexture : disabledTexture
        }
    }
}
var isSelected: Bool = false {
    didSet {
        texture = isSelected ? selectedTexture : defaultTexture
    }
}
var defaultTexture: SKTexture
var selectedTexture: SKTexture
var label: SKLabelNode


required init(coder: NSCoder) {
    fatalError("NSCoding not supported")
}

init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {

    self.defaultTexture = defaultTexture
    self.selectedTexture = selectedTexture
    self.disabledTexture = disabledTexture
    self.label = SKLabelNode(fontNamed: "Helvetica");
    super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())
    userInteractionEnabled = true


    self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Center;
    self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center;
    addChild(self.label)

    // Adding this node as an empty layer. Without it the touch functions are not being called
    // The reason for this is unknown when this was implemented...?
    let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
    bugFixLayerNode.position = self.position
    addChild(bugFixLayerNode)

}

/**
* Taking a target object and adding an action that is triggered by a button event.
*/
func setButtonAction(target: AnyObject, triggerEvent event:FTButtonActionType, action:Selector) {

    switch (event) {
    case .TouchUpInside:
        targetTouchUpInside = target
        actionTouchUpInside = action
    case .TouchDown:
        targetTouchDown = target
        actionTouchDown = action
    case .TouchUp:
        targetTouchUp = target
        actionTouchUp = action
    }

}


func setButtonLabel(#title: NSString, font: String, fontSize: CGFloat) {
    var title = title;
    var font = font;
    var fontSize = fontSize;

    self.label.text = title;
    self.label.fontSize = fontSize;
    self.label.fontName = font;

}

var disabledTexture: SKTexture?
var actionTouchUpInside: Selector?
var actionTouchUp: Selector?
var actionTouchDown: Selector?
weak var targetTouchUpInside: AnyObject?
weak var targetTouchUp: AnyObject?
weak var targetTouchDown: AnyObject?

override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)  {
    let touch: AnyObject! = touches.anyObject()
    let touchLocation = touch.locationInNode(parent)

    if (!isEnabled) {
        return
    }
    isSelected = true
    if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
        UIApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self, forEvent: nil)
    }


}

override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!)  {

    if (!isEnabled) {
        return
    }

    let touch: AnyObject! = touches.anyObject()
    let touchLocation = touch.locationInNode(parent)

    if (CGRectContainsPoint(frame, touchLocation)) {
        isSelected = true
    } else {
        isSelected = false
    }

}

override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {

    if (!isEnabled) {
        return
    }

    isSelected = false

    if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
        let touch: AnyObject! = touches.anyObject()
        let touchLocation = touch.locationInNode(parent)

        if (CGRectContainsPoint(frame, touchLocation) ) {
            UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
        }

    }

    if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
        UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
    }
}

}

@richy 2015-11-08 22:48:24

Updated the code for swift 2.1 here: gist.github.com/richy486/5d408c442ac1c0c2891f

@jglasse 2017-01-14 01:57:19

..and I updated to Swift 3 here: github.com/jglasse/SKButtonSwift3

@kartpick 2015-09-25 04:40:08

Graf`s solution has one issue. For example:

self.pauseButton = [[AGSKBButtonNode alloc] initWithImageNamed:@"ButtonPause"];
self.pauseButton.position = CGPointMake(0, 0);
[self.pauseButton setTouchUpInsideTarget:self action:@selector(pauseButtonPressed)];

[_hudLayer addChild:_pauseButton];

_hudLayer is a SKNode, a property of my scene. So, you`ll get exception, because of method touchesEnded in SKButton. It will call [SKSpriteNode pauseButtonPressed], not with scene.

The solution to change self.parent to touch target:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInNode:self.parent];

if ([self isEnabled] && CGRectContainsPoint(self.frame, touchPoint)) {
    if (_actionTouchUpInside){
        [_targetTouchUpInside performSelectorOnMainThread:_actionTouchUpInside withObject:_targetTouchUpInside waitUntilDone:YES];
    }
}
[self setIsSelected:NO];
if (_actionTouchUp){
    [_targetTouchUp performSelectorOnMainThread:_actionTouchUp withObject:_targetTouchUp waitUntilDone:YES];
}}

@user1190882 2015-08-07 19:11:23

I have subclassed SKScene class and achieved the problem of solving button taps in this project.

https://github.com/Prasad9/SpriteKitButton

In it, all the nodes which are necessary to be known upon tapped should be named.

In addition to detecting button tap, this project also enables you to detect whether the touch on a particular node has started or ended.

To get tap action, override the following method in your Scene file.

- (void)touchUpInsideOnNodeName:(NSString *)nodeName atPoint:(CGPoint)touchPoint {
    // Your code here.
 }

To get to know the start of touch on a particular body, override the following method in your Scene file.

 - (void)touchBeginOnNodeName:(NSString *)nodeName {
    // Your code here.
 }

To get to know the end of touch on a particular body, override the following method in your Scene file.

 - (void)touchEndedOnNodeName:(NSString *)nodeName {
    // Your code here.
 }

@txaidw 2015-04-21 17:54:15

My solution to solve this problem written completely in SWIFT, using closures.

Its pretty simple to use! https://github.com/txaidw/TWControls

class Test {
    var testProperty = "Default String"

    init() {
        let control = TWButton(normalColor: SKColor.blueColor(), highlightedColor: SKColor.redColor(), size: CGSize(width: 160, height: 80))
        control.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
        control.position.allStatesLabelText = "PLAY"
        control.addClosureFor(.TouchUpInside, target: self, closure: { (scene, sender) -> () in
            scene.testProperty = "Changed Property"
        })
    }

    deinit { println("Class Released..") }
}

@Maury Markowitz 2015-02-27 23:27:01

And since all of us aren't targeting iOS, here's the start of some code I wrote to handle mouse interaction on the Mac.

Question for the gurus: does MacOS offer touch events when using a trackpad? Or are these sent into SpriteKit as mouse events?

Another question for the gurus, shouldn't this class properly be called SKButtonNode?

Anyway, try this...

#if os(iOS)
    override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)  {
        let touch: AnyObject! = touches.anyObject()
        let touchLocation = touch.locationInNode(parent)

        if (!isEnabled) { return }

        isSelected = true
        if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
            UIApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self, forEvent: nil)
        }
    }

    override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!)  {
        if (!isEnabled) { return }

        let touch: AnyObject! = touches.anyObject()
        let touchLocation = touch.locationInNode(parent)

        if (CGRectContainsPoint(frame, touchLocation)) {
            isSelected = true
        } else {
            isSelected = false
        }
    }

    override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
        if (!isEnabled) { return }

        isSelected = false

        if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
            let touch: AnyObject! = touches.anyObject()
            let touchLocation = touch.locationInNode(parent)

            if (CGRectContainsPoint(frame, touchLocation) ) {
                UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
            }
        }

        if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
            UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
        }
    }
#else

    // FIXME: needs support for mouse enter and leave, turning on and off selection

    override func mouseDown(event: NSEvent) {
        if (!isEnabled) { return }

        if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
            NSApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self)
        }
    }

    override func mouseUp(event: NSEvent) {
        if (!isEnabled) { return }

        if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
            let touchLocation = event.locationInNode(parent)

            if (CGRectContainsPoint(frame, touchLocation) ) {
                NSApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self)
            }
        }

        if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
            NSApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self)
        }
    }
#endif

@CodyMace 2015-08-04 03:41:58

As far as I know Spritekit for OSX only observes mouse stuff :/ And yeah, it probably should have the word Node on the end. Like SKLabelNode.

@Guillaume Laurent 2015-02-11 16:58:01

Here's another version based on Filip's Swift code. I've just simplified it a little and allowed it to take blocks rather than only selectors :

import Foundation
import SpriteKit

enum FTButtonTarget {
    case aSelector(Selector, AnyObject)
    case aBlock(() -> Void)
}

class FTButtonNode: SKSpriteNode {

    var actionTouchUp : FTButtonTarget?
    var actionTouchUpInside : FTButtonTarget?
    var actionTouchDown : FTButtonTarget?

    var isEnabled: Bool = true {
        didSet {
            if (disabledTexture != nil) {
                texture = isEnabled ? defaultTexture : disabledTexture
            }
        }
    }
    var isSelected: Bool = false {
        didSet {
            texture = isSelected ? selectedTexture : defaultTexture
        }
    }

    var defaultTexture: SKTexture
    var selectedTexture: SKTexture

    required init(coder: NSCoder) {
        fatalError("NSCoding not supported")
    }

init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {

    self.defaultTexture = defaultTexture
    self.selectedTexture = selectedTexture
    self.disabledTexture = disabledTexture

    super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())

    userInteractionEnabled = true

    // Adding this node as an empty layer. Without it the touch functions are not being called
    // The reason for this is unknown when this was implemented...?
    let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
    bugFixLayerNode.position = self.position
    addChild(bugFixLayerNode)

}

var disabledTexture: SKTexture?

func callTarget(buttonTarget:FTButtonTarget) {

    switch buttonTarget {
    case let .aSelector(selector, target):
        if target.respondsToSelector(selector) {
            UIApplication.sharedApplication().sendAction(selector, to: target, from: self, forEvent: nil)
        }
    case let .aBlock(block):
        block()
    }

}

override func touchesBegan(touches: NSSet, withEvent event: UIEvent)  {
    let touch: AnyObject! = touches.anyObject()
    let touchLocation = touch.locationInNode(parent)

    if (!isEnabled) {
        return
    }
    isSelected = true

    if let act = actionTouchDown {
        callTarget(act)
    }

}

override func touchesMoved(touches: NSSet, withEvent event: UIEvent)  {

    if (!isEnabled) {
        return
    }

    let touch: AnyObject! = touches.anyObject()
    let touchLocation = touch.locationInNode(parent)

    if (CGRectContainsPoint(frame, touchLocation)) {
        isSelected = true
    } else {
        isSelected = false
    }

}

 override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

     if (!isEnabled) {
         return
     }

     isSelected = false

     let touch: AnyObject! = touches.anyObject()
     let touchLocation = touch.locationInNode(parent)

     if (CGRectContainsPoint(frame, touchLocation) ) {

         if let act = actionTouchUpInside {
             callTarget(act)
         }
     }

     if let act = actionTouchUp {
         callTarget(act)
     }
 }
}

Use it like this :

       aFTButton.actionTouchUpInside = FTButtonTarget.aBlock({ () -> Void in
        println("button touched")
    })

Hope this helps.

@ZeMoon 2014-06-18 15:08:23

I had created a class for using SKSpriteNode as a button quite a while ago. You can find it on GitHub here.

AGSpriteButton

It's implementation is based on UIButton, so if you are already familiar with iOS, you should find it easy to work with.

It can also be assigned a block or an SKAction to be executed when the button is pressed.

It includes a method to set up a label as well.

A button will typically be declared like so:

AGSpriteButton *button = [AGSpriteButton buttonWithColor:[UIColor redColor] andSize:CGSizeMake(300, 100)];
[button setLabelWithText:@"Button Text" andFont:nil withColor:nil];
button.position = CGPointMake(self.size.width / 2, self.size.height / 3);
[button addTarget:self selector:@selector(someSelector) withObject:nil forControlEvent:AGButtonControlEventTouchUpInside];
[self addChild:button];

And that's it. You're good to go.

@Maury Markowitz 2015-02-21 20:14:26

Is there any reason we couldn't use SKColor instead of UIColor? If we use UIColor we're stuck on iOS.

@ZeMoon 2015-02-21 20:15:14

You can as easily use SKColor instead of UIColor

@bshirley 2014-10-29 22:06:23

If you desire, you can use UIButton (or any other UIView).

When a SKScene is created, it doesn't yet exist in an SKView. You should implement didMoveToView: on your SKScene subclass. At this point, you have access to the SKView the scene is placed in and you can add UIKit objects to it. For prettiness, I faded them in …

- (void)didMoveToView:(SKView *)view {
  UIView *b = [self _createButton];  // <-- performs [self.view addSubview:button]
  // create other UI elements, also add them to the list to remove …
  self.customSubviews = @[b];

  b.alpha = 0;

  [UIView animateWithDuration:0.4
                        delay:2.4
                      options:UIViewAnimationOptionCurveEaseIn
                   animations:^{
                     b.alpha = 1;
                   } completion:^(BOOL finished) {
                     ;
                   }];
}

you will need to deliberately remove them from the scene when you transition away, unless of course it makes total sense for them to remain there.

- (void)removeCustomSubviews {
  for (UIView *v in self.customSubviews) {
    [UIView animateWithDuration:0.2
                          delay:0
                        options:UIViewAnimationOptionCurveEaseIn
                     animations:^{
                       v.alpha = 0;
                   } completion:^(BOOL finished) {
                       [v removeFromSuperview];
                 }];
  }
}

For those unfamiliar with programmatically creating a UIButton, here one example (you could do a 100 things differently here) …

- (UIButton *)_createButton {
  UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
  [b setTitle:@"Continue" forState:UIControlStateNormal];
  [b setBackgroundImage:[UIImage imageNamed:@"GreenButton"] forState:UIControlStateNormal];
  [b setBackgroundImage:[UIImage imageNamed:@"GreenButtonSelected"] forState:UIControlStateHighlighted];
  b.titleLabel.adjustsFontSizeToFitWidth = YES;
  b.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:36];
  b.frame = CGRectMake(self.size.width * .7, self.size.height * .2, self.size.width * .2, self.size.height * .1);
  [b addTarget:self action:@selector(continuePlay) forControlEvents:UIControlEventTouchUpInside];
  [self.view addSubview:b];

  return b;
}

Reminder: UIView origin is in the upper left, SKScene origin is in the lower left.

@Groot 2014-08-04 09:43:09

For people writing their games in Swift! I have rewritten the essential parts of Graf's solution to a swift class. Hope it helps:

import Foundation
import SpriteKit

class FTButtonNode: SKSpriteNode {

    enum FTButtonActionType: Int {
        case TouchUpInside = 1,
        TouchDown, TouchUp
    }

    var isEnabled: Bool = true {
    didSet {
        if (disabledTexture != nil) {
            texture = isEnabled ? defaultTexture : disabledTexture
        }
    }
    }
    var isSelected: Bool = false {
    didSet {
        texture = isSelected ? selectedTexture : defaultTexture
    }
    }
    var defaultTexture: SKTexture
    var selectedTexture: SKTexture

    required init(coder: NSCoder) {
        fatalError("NSCoding not supported")
    }

    init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {

        self.defaultTexture = defaultTexture
        self.selectedTexture = selectedTexture
        self.disabledTexture = disabledTexture

        super.init(texture: defaultTexture, color: UIColor.whiteColor(), size: defaultTexture.size())

        userInteractionEnabled = true

        // Adding this node as an empty layer. Without it the touch functions are not being called
        // The reason for this is unknown when this was implemented...?
        let bugFixLayerNode = SKSpriteNode(texture: nil, color: nil, size: defaultTexture.size())
        bugFixLayerNode.position = self.position
        addChild(bugFixLayerNode)

    }

    /**
    * Taking a target object and adding an action that is triggered by a button event.
    */
    func setButtonAction(target: AnyObject, triggerEvent event:FTButtonActionType, action:Selector) {

        switch (event) {
        case .TouchUpInside:
            targetTouchUpInside = target
            actionTouchUpInside = action
        case .TouchDown:
            targetTouchDown = target
            actionTouchDown = action
        case .TouchUp:
            targetTouchUp = target
            actionTouchUp = action
        }

    }

    var disabledTexture: SKTexture?
    var actionTouchUpInside: Selector?
    var actionTouchUp: Selector?
    var actionTouchDown: Selector?
    weak var targetTouchUpInside: AnyObject?
    weak var targetTouchUp: AnyObject?
    weak var targetTouchDown: AnyObject?

    override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)  {
        let touch: AnyObject! = touches.anyObject()
        let touchLocation = touch.locationInNode(parent)

        if (!isEnabled) {
            return
        }
        isSelected = true
        if (targetTouchDown != nil && targetTouchDown!.respondsToSelector(actionTouchDown!)) {
            UIApplication.sharedApplication().sendAction(actionTouchDown!, to: targetTouchDown, from: self, forEvent: nil)
        }


    }

    override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!)  {

        if (!isEnabled) {
            return
        }

        let touch: AnyObject! = touches.anyObject()
        let touchLocation = touch.locationInNode(parent)

        if (CGRectContainsPoint(frame, touchLocation)) {
            isSelected = true
        } else {
            isSelected = false
        }

    }

    override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {

        if (!isEnabled) {
            return
        }

        isSelected = false

        if (targetTouchUpInside != nil && targetTouchUpInside!.respondsToSelector(actionTouchUpInside!)) {
            let touch: AnyObject! = touches.anyObject()
            let touchLocation = touch.locationInNode(parent)

            if (CGRectContainsPoint(frame, touchLocation) ) {
                UIApplication.sharedApplication().sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, forEvent: nil)
            }

        }

        if (targetTouchUp != nil && targetTouchUp!.respondsToSelector(actionTouchUp!)) {
            UIApplication.sharedApplication().sendAction(actionTouchUp!, to: targetTouchUp, from: self, forEvent: nil)
        }
    }

}

@user3204765 2014-01-27 00:12:10

I have used SKButton class by Graf.

I use the SKButton to do scene navigation. i.e present another scene when the user press the SKButton. I get EXC_BAD_ACCESS error at touchesEnded->[self setIsSelected:NO]. This happens especially frequently on the latest iPad with fast CPU.

After checking and troubleshooting, I realised that the SKButton object is already "deallocated" when the setIsSelected function is being called. This is because I use the SKButton to navigate to next scene and this also means that the current scene can be deallocated any time.

I made a small change by putting the setIsSelected in the "else" portion as follows.

Hope this helps for other developer who also see the same error.

(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInNode:self.parent];

    if ([self isEnabled] && CGRectContainsPoint(self.frame, touchPoint)) {
        objc_msgSend(_targetTouchUpInside, _actionTouchUpInside);
    } else {
       [self setIsSelected:NO];
    }
    objc_msgSend(_targetTouchUp, _actionTouchUp);
}

@Uli Köhler 2014-01-27 00:34:38

Please format your post & sourcecode, it is very hard to read this way!

@AndyOS 2013-09-29 19:49:16

you could use a SKSpriteNode as your button, and then when the user touches, check if that was the node touched. Use the SKSpriteNode's name property to identify the node:

//fire button
- (SKSpriteNode *)fireButtonNode
{
    SKSpriteNode *fireNode = [SKSpriteNode spriteNodeWithImageNamed:@"fireButton.png"];
    fireNode.position = CGPointMake(fireButtonX,fireButtonY);
    fireNode.name = @"fireButtonNode";//how the node is identified later
    fireNode.zPosition = 1.0;
    return fireNode;
}

Add node to your scene:

[self addChild: [self fireButtonNode]];

Handle touches:

//handle touch events
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
    SKNode *node = [self nodeAtPoint:location];

    //if fire button touched, bring the rain
    if ([node.name isEqualToString:@"fireButtonNode"]) {
         //do whatever...
    }
}

@DogCoffee 2013-10-19 00:13:45

If you add an iVar as the button, you could remove the name checking and just use if ([_fireNode containsPoint:location]) does the same thing just different.

@Esteban Bouza 2013-12-12 19:19:21

Comparing strings it's a dirty solution. Although @Smick 's solution is better, isn't there other cleaner way to achieve this?

@Omer Obaid 2014-02-13 07:29:22

hey we can not add button in SpriteKit like SkLabelNode?

@duxfox-- 2014-11-30 18:07:56

does this allow multi touch events? for example 2 buttons clicked simultaneously? one of them a movement button, another is a fire button.

Related Questions

Sponsored Content

1 Answered Questions

[SOLVED] UIButton events. What's the difference?

10 Answered Questions

[SOLVED] How to set the title of UIButton as left alignment?

  • 2010-05-04 11:56:19
  • Madan Mohan
  • 189067 View
  • 434 Score
  • 10 Answer
  • Tags:   objective-c uibutton

1 Answered Questions

[SOLVED] Translate location in UIView to SKScene

3 Answered Questions

Xcode, objective Class template missing subclass of SKScene

3 Answered Questions

[SOLVED] How to get the title for a UIButton when it is pressed

1 Answered Questions

Present an SKScene from itself

1 Answered Questions

[SOLVED] Using NSNotificationCenter with SkScene to UIViewController

1 Answered Questions

SKScene Leaderboards

2 Answered Questions

[SOLVED] Custom button class setuserinteractionenabled

1 Answered Questions

Gaming SKScene transitions Swift

  • 2014-09-16 17:33:32
  • Emanuel Gallardo
  • 1244 View
  • 0 Score
  • 1 Answer
  • Tags:   ios swift sprite-kit

Sponsored Content