Setting up buttons in SKScene - ios
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.
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...
}
}
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'
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)
}
}
}
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.
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);
}
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.
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)
}
}
}
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
}
}
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..") }
}
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.
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
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.
}
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];
}}
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)
}
}
I wasn't convinced of any of the above options, so based on the latest Swift4 I created my own solution.
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.
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)
}
}
Related
Custom UIButton .touchDragEnter and .touchDragExit area/size?
Is it possible to customize the area from the button at which it is considered .touchDragExit (or .touchDragEnter) (out of its selectable area?)? To be more specific, I am speaking about this situation: I tap the UIButton, the .touchDown gets called, then I start dragging my finger away from the button and at some point (some distance away) it will not select anymore (and of course I can drag back in to select...). I would like the modify that distance... Is this even possible?
You need to overwrite the UIButton continueTracking and touchesEnded functions. Adapting #Dean's link, the implementation would be as following (swift 4.2): class ViewController: UIViewController { #IBOutlet weak var button: DragButton! override func viewDidLoad() { super.viewDidLoad() } } class DragButton: UIButton { private let _boundsExtension: CGFloat = 0 // Adjust this as needed override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension)) let currentLocation: CGPoint = touch.location(in: self) let previousLocation: CGPoint = touch.previousLocation(in: self) let touchOutside: Bool = !outerBounds.contains(currentLocation) if touchOutside { let previousTouchInside: Bool = outerBounds.contains(previousLocation) if previousTouchInside { print("touchDragExit") sendActions(for: .touchDragExit) } else { print("touchDragOutside") sendActions(for: .touchDragOutside) } } else { let previousTouchOutside: Bool = !outerBounds.contains(previousLocation) if previousTouchOutside { print("touchDragEnter") sendActions(for: .touchDragEnter) } else { print("touchDragInside") sendActions(for: .touchDragInside) } } return true } override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { let touch: UITouch = touches.first! let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension)) let currentLocation: CGPoint = touch.location(in: self) let touchInside: Bool = outerBounds.contains(currentLocation) if touchInside { print("touchUpInside action") return sendActions(for: .touchUpInside) } else { print("touchUpOutside action") return sendActions(for: .touchUpOutside) } } } Try changing the _boundsExtension value
The drag area is exaclty equal to the area define by bounds. So if you want to customize the drag are simple customise the bounds of your button.
Scrollview stopping at a certain point
Hi I currently have a horizontal UIScrollView that allows me to pick a character in my game and then select it but the problem that I have now is I am trying to get the scrollview to stop at the point where the sprite/character is in the middle of the screen and instead of having it stop anywhere. My UIScrollView has a “moveable node” that contains multiple pages that hold all the sprites as seen below: scrollViewHorizontal = CustomScrollView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height), scene: self, moveableNode: moveableNodeHorizontal, scrollDirection: .Horizontal) scrollViewHorizontal.contentSize = CGSizeMake(self.frame.size.width * 4, self.frame.size.height) // * 4 makes it three times as wide as screen view?.addSubview(scrollViewHorizontal) scrollViewHorizontal.hidden = true addChild(moveableNodeHorizontal) moveableNodeHorizontal.hidden = true moveableNodeHorizontal.zPosition = 100000 scrollViewHorizontal.setContentOffset(CGPoint(x: 0 + self.frame.size.width + self.frame.size.width * 2, y: 0), animated: false) let page1ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(scrollViewHorizontal.frame.size.width, scrollViewHorizontal.frame.size.height)) page1ScrollView.zPosition = -1 page1ScrollView.position = CGPointMake(CGRectGetMidX(self.frame) - (self.frame.size.width * 3.5), CGRectGetMidY(self.frame) - (self.frame.height / 2)) moveableNodeHorizontal.addChild(page1ScrollView) let page2ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(scrollViewHorizontal.frame.size.width, scrollViewHorizontal.frame.size.height)) page2ScrollView.zPosition = -1 page2ScrollView.position = CGPointMake(CGRectGetMidX(self.frame) - (self.frame.size.width * 2.5), CGRectGetMidY(self.frame) - (self.frame.height / 2)) moveableNodeHorizontal.addChild(page2ScrollView) Characters.append(generateCharacters(CGPointMake(0, 0), page:(page1ScrollView), tex: "YellowFrog")) Characters.append(generateCharacters(CGPointMake(100, 0), page:(page1ScrollView), tex: "Frog")) Characters.append(generateCharacters(CGPointMake(0, 0), page:(page2ScrollView), tex: "ball")) Characters.append(generateCharacters(CGPointMake(100, 0), page:(page2ScrollView), tex: "ball")) I searched this previously before asking the question and it was recommended that I use this: func scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { But I have no idea how to implement it into my code because my scrollview is contained in another class which can be seen here: /// Nodes touched var nodesTouched: [AnyObject] = [] // global /// Scroll direction enum ScrollDirection: Int { case None = 0 case Vertical case Horizontal } /// Custom UIScrollView class class CustomScrollView: UIScrollView { // MARK: - Static Properties /// Touches allowed static var disabledTouches = false /// Scroll view private static var scrollView: UIScrollView! // MARK: - Properties /// Nodes touched. This will forward touches to node subclasses. private var nodesTouched = [AnyObject]() /// Current scene private let currentScene: SKScene /// Moveable node private let moveableNode: SKNode /// Scroll direction private let scrollDirection: ScrollDirection // MARK: - Init init(frame: CGRect, scene: SKScene, moveableNode: SKNode, scrollDirection: ScrollDirection) { self.currentScene = scene self.moveableNode = moveableNode self.scrollDirection = scrollDirection super.init(frame: frame) CustomScrollView.scrollView = self self.frame = frame delegate = self indicatorStyle = .White scrollEnabled = true userInteractionEnabled = true //canCancelContentTouches = false //self.minimumZoomScale = 1 //self.maximumZoomScale = 3 if scrollDirection == .Horizontal { let flip = CGAffineTransformMakeScale(-1,-1) transform = flip } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - Touches extension CustomScrollView { /// Began override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { //super.touchesBegan(touches, withEvent: event) for touch in touches { let location = touch.locationInNode(currentScene) guard !CustomScrollView.disabledTouches else { return } /// Call touches began in current scene currentScene.touchesBegan(touches, withEvent: event) /// Call touches began in all touched nodes in the current scene nodesTouched = currentScene.nodesAtPoint(location) for node in nodesTouched { node.touchesBegan(touches, withEvent: event) } } } /// Moved override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { //super.touchesMoved(touches, withEvent: event) for touch in touches { let location = touch.locationInNode(currentScene) guard !CustomScrollView.disabledTouches else { return } /// Call touches moved in current scene currentScene.touchesMoved(touches, withEvent: event) /// Call touches moved in all touched nodes in the current scene nodesTouched = currentScene.nodesAtPoint(location) for node in nodesTouched { node.touchesMoved(touches, withEvent: event) } } } /// Ended override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { //super.touchesEnded(touches, withEvent: event) for touch in touches { let location = touch.locationInNode(currentScene) guard !CustomScrollView.disabledTouches else { return } /// Call touches ended in current scene currentScene.touchesEnded(touches, withEvent: event) /// Call touches ended in all touched nodes in the current scene nodesTouched = currentScene.nodesAtPoint(location) for node in nodesTouched { node.touchesEnded(touches, withEvent: event) } } } /// Cancelled override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) { //super.touchesCancelled(touches, withEvent: event) for touch in touches! { let location = touch.locationInNode(currentScene) guard !CustomScrollView.disabledTouches else { return } /// Call touches cancelled in current scene currentScene.touchesCancelled(touches, withEvent: event) /// Call touches cancelled in all touched nodes in the current scene nodesTouched = currentScene.nodesAtPoint(location) for node in nodesTouched { node.touchesCancelled(touches, withEvent: event) } } } } // MARK: - Touch Controls extension CustomScrollView { /// Disable class func disable() { CustomScrollView.scrollView?.userInteractionEnabled = false CustomScrollView.disabledTouches = true } /// Enable class func enable() { CustomScrollView.scrollView?.userInteractionEnabled = true CustomScrollView.disabledTouches = false } } // MARK: - Delegates extension CustomScrollView: UIScrollViewDelegate { func scrollViewDidScroll(scrollView: UIScrollView) { if scrollDirection == .Horizontal { moveableNode.position.x = scrollView.contentOffset.x } else { moveableNode.position.y = scrollView.contentOffset.y } } } EDIT: extension CustomScrollView: UIScrollViewDelegate { func scrollViewDidScroll(scrollView: UIScrollView) { if scrollDirection == .Horizontal { moveableNode.position.x = scrollView.contentOffset.x } else { moveableNode.position.y = scrollView.contentOffset.y } } }
CustomScrollView is a project showed how UIKit could be used in sprite-kit but this causes a lot of work during rendering that slows your game (due to low fps) so it's advisable and reasonable to use it just only for your game menus, not for the game. A way to iOS8.x: You can build for example many SKSpriteNode backgrounds that follow the one with the other and in your update method doing: override func update(currentTime: CFTimeInterval) { self.enumerateChildNodesWithName("background") { node, _ in if node is SKSpriteNode { if let p = (node as! SKShapeNode).position { // adjust your background positions to make the scroll } } } } A way to iOS9x: Apple added the SKCameraNode class to sprite-kit in iOS 9 to make it easier to scroll and zoom in sprite-kit scenes. var gameCamera = SKCameraNode() override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { for touch in touches { let location = touch.locationInNode(self) let previousLocation = touch.previousLocationInNode(self) let deltaX = location.x - previousLocation.x gameCamera.position.x += deltaX } } The CustomScrollView page selector. Having said that we come to your situation, suppose you have all your pages in array called pages and you want to stop the scroll to the x page: func scrollToPage(page:Int) { let totalPages : Int = (pages.count - 1) let reversedPage : Int = totalPages - page self.scrollView.setContentOffset(CGPointMake(round(self.frame.size.width*CGFloat(reversedPage)), scrollView.contentOffset.y),animated: true) } Another good idea it's to intercept the scroll delegate methods to update your elements: func scrollViewDidScroll(scrollView: UIScrollView) { self.scrollView.scrollViewDidScroll(scrollView) updateLayout() // call my method to update elements in scroll checking the current page } You could obtain your page number whit this code: extension UIScrollView { var currentPage: Int { return abs(Int(round((contentSize.width - self.contentOffset.x) / self.bounds.size.width))-1) } }
3D touch/Force touch implementation
How can we implement 3D touch to check if the user taps on UIView or force touch on UIView? Is there a way to do this with UIGestureRecognize or only with UITouch?
You can do it without a designated gesture recognizer. You do not need to adjust the touchesEnded and touchesBegan method, but simply the touchesMoved to obtain the correct values. getting the force of a uitouch from began/ended will return weird values. UITouch *touch = [touches anyObject]; CGFloat maximumPossibleForce = touch.maximumPossibleForce; CGFloat force = touch.force; CGFloat normalizedForce = force/maximumPossibleForce; then, set a force threshold and compare the normalizedForce to this threshold (0.75 seems fine for me).
The 3D Touch properties are available on UITouch objects. You can get these touches by overriding a UIView's touchesBegan: and touchesMoved: methods. Not sure what you see in touchesEnded: yet. If you're willing to create new gesture recognizers, you have full access to the UITouches as exposed in UIGestureRecognizerSubclass. I'm not sure how you could use the 3D touch properties in a traditional UIGestureRecognizer. Maybe via the UIGestureRecognizerDelegate protocol's gestureRecognizer:shouldReceiveTouch: method.
With Swift 4.2 and iOS 12, a possible way to solve your problem is to create a custom subclass of UIGestureRecognizer that handles Force Touch and add it to your view next to a UITapGestureRecognizer. The following complete code shows how to implement it: ViewController.swift import UIKit class ViewController: UIViewController { let redView = UIView() lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler)) lazy var forceTouchGestureRecognizer = ForceTouchGestureRecognizer(target: self, action: #selector(forceTouchHandler)) override func viewDidLoad() { super.viewDidLoad() redView.backgroundColor = .red redView.addGestureRecognizer(tapGestureRecognizer) view.addSubview(redView) redView.translatesAutoresizingMaskIntoConstraints = false redView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true redView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true redView.widthAnchor.constraint(equalToConstant: 100).isActive = true redView.heightAnchor.constraint(equalToConstant: 100).isActive = true } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.forceTouchCapability == UIForceTouchCapability.available { redView.addGestureRecognizer(forceTouchGestureRecognizer) } else { // When force touch is not available, remove force touch gesture recognizer. // Also implement a fallback if necessary (e.g. a long press gesture recognizer) redView.removeGestureRecognizer(forceTouchGestureRecognizer) } } #objc func tapHandler(_ sender: UITapGestureRecognizer) { print("Tap triggered") } #objc func forceTouchHandler(_ sender: ForceTouchGestureRecognizer) { UINotificationFeedbackGenerator().notificationOccurred(.success) print("Force touch triggered") } } ForceTouchGestureRecognizer.swift import UIKit.UIGestureRecognizerSubclass #available(iOS 9.0, *) final class ForceTouchGestureRecognizer: UIGestureRecognizer { private let threshold: CGFloat = 0.75 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesBegan(touches, with: event) if let touch = touches.first { handleTouch(touch) } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) if let touch = touches.first { handleTouch(touch) } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesEnded(touches, with: event) state = UIGestureRecognizer.State.failed } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesCancelled(touches, with: event) state = UIGestureRecognizer.State.failed } private func handleTouch(_ touch: UITouch) { guard touch.force != 0 && touch.maximumPossibleForce != 0 else { return } if touch.force / touch.maximumPossibleForce >= threshold { state = UIGestureRecognizer.State.recognized } } } Sources: GitHub / FlexMonkey - DeepPressGestureRecognizer GitHub / ashleymills - ForceTouchGestureRecognizer Apple Developer Documentation - Implementing a Discrete Gesture Recognizer
I created a UIGestureRecognizer that emulates the behavior of the Apple Mail app. Upon 3D touch, it starts with a short single pulse vibrate and then an optional secondary action (hardTarget) and pulse called by hard pressing shortly after the initial press. Adapted from https://github.com/FlexMonkey/DeepPressGestureRecognizer Changes: 3D touch vibrate pulses like iOS system behavior touch must come up for it to end, like Apple mail app threshold defaults to system default level hard touch triggers hardAction call like mail app Note: I added the undocumented system sound k_PeakSoundID, but feel free to turn that off if you are uncomfortable using a constant beyond the documented range. I have been using system sounds with undisclosed constants for years, but you are welcomed to turn off the vibration pulses using the vibrateOnDeepPress property. import UIKit import UIKit.UIGestureRecognizerSubclass import AudioToolbox class DeepPressGestureRecognizer: UIGestureRecognizer { var vibrateOnDeepPress = true var threshold: CGFloat = 0.75 var hardTriggerMinTime: TimeInterval = 0.5 var onDeepPress: (() -> Void)? private var deepPressed: Bool = false { didSet { if (deepPressed && deepPressed != oldValue) { onDeepPress?() } } } private var deepPressedAt: TimeInterval = 0 private var k_PeakSoundID: UInt32 = 1519 private var hardAction: Selector? private var target: AnyObject? required init(target: AnyObject?, action: Selector, hardAction: Selector? = nil, threshold: CGFloat = 0.75) { self.target = target self.hardAction = hardAction self.threshold = threshold super.init(target: target, action: action) } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { if let touch = touches.first { handle(touch: touch) } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { if let touch = touches.first { handle(touch: touch) } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesEnded(touches, with: event) state = deepPressed ? UIGestureRecognizerState.ended : UIGestureRecognizerState.failed deepPressed = false } private func handle(touch: UITouch) { guard let _ = view, touch.force != 0 && touch.maximumPossibleForce != 0 else { return } let forcePercentage = (touch.force / touch.maximumPossibleForce) let currentTime = Date.timeIntervalSinceReferenceDate if !deepPressed && forcePercentage >= threshold { state = UIGestureRecognizerState.began if vibrateOnDeepPress { AudioServicesPlaySystemSound(k_PeakSoundID) } deepPressedAt = Date.timeIntervalSinceReferenceDate deepPressed = true } else if deepPressed && forcePercentage <= 0 { endGesture() } else if deepPressed && currentTime - deepPressedAt > hardTriggerMinTime && forcePercentage == 1.0 { endGesture() if vibrateOnDeepPress { AudioServicesPlaySystemSound(k_PeakSoundID) } //fire hard press if let hardAction = self.hardAction, let target = self.target { _ = target.perform(hardAction, with: self) } } } func endGesture() { state = UIGestureRecognizerState.ended deepPressed = false } } // MARK: DeepPressable protocol extension protocol DeepPressable { var gestureRecognizers: [UIGestureRecognizer]? {get set} func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer) func removeGestureRecognizer(gestureRecognizer: UIGestureRecognizer) func setDeepPressAction(target: AnyObject, action: Selector) func removeDeepPressAction() } extension DeepPressable { func setDeepPressAction(target: AnyObject, action: Selector) { let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: target, action: action, threshold: 0.75) self.addGestureRecognizer(gestureRecognizer: deepPressGestureRecognizer) } func removeDeepPressAction() { guard let gestureRecognizers = gestureRecognizers else { return } for recogniser in gestureRecognizers where recogniser is DeepPressGestureRecognizer { removeGestureRecognizer(gestureRecognizer: recogniser) } } }
The way I am doing this is to use a combination of a UITapGestureRecognizer (provided by Apple) and a DFContinuousForceTouchGestureRecognizer (provided by me). The DFContinuousForceTouchGestureRecognizer is nice because it provides continuous updates about the pressure changes so you can do things like augment the view as the user varies their pressure on it, as opposed to a single event. If you just want a single event you can ignore eveything in the DFContinuousForceTouchDelegate except the - (void) forceTouchRecognized callback. https://github.com/foggzilla/DFContinuousForceTouchGestureRecognizer You can download this and run the sample app on a device that supports force press to see how it feels. In your UIViewController implement the following: - (void)viewDidLoad { [super viewDidLoad]; _forceTouchRecognizer = [[DFContinuousForceTouchGestureRecognizer alloc] init]; _forceTouchRecognizer.forceTouchDelegate = self; //here to demonstrate how this works alonside a tap gesture recognizer _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)]; [self.imageView addGestureRecognizer:_tapGestureRecognizer]; [self.imageView addGestureRecognizer:_forceTouchRecognizer]; } implement selector for tap gesture #pragma UITapGestureRecognizer selector - (void)tapped:(id)sender { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [[[UIAlertView alloc] initWithTitle:#"Tap" message:#"YEAH!!" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] show]; }); } Implement the delegate protocol for force touch: #pragma DFContinuousForceTouchDelegate - (void)forceTouchRecognized:(DFContinuousForceTouchGestureRecognizer *)recognizer { self.imageView.transform = CGAffineTransformIdentity; [self.imageView setNeedsDisplay]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [[[UIAlertView alloc] initWithTitle:#"Force Touch" message:#"YEAH!!" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] show]; }); } - (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didStartWithForce:(CGFloat)force maxForce:(CGFloat)maxForce { CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f); self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta); [self.imageView setNeedsDisplay]; } - (void) forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didMoveWithForce:(CGFloat)force maxForce:(CGFloat)maxForce { CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f); self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta); [self.imageView setNeedsDisplay]; } - (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didCancelWithForce:(CGFloat)force maxForce:(CGFloat)maxForce { self.imageView.transform = CGAffineTransformIdentity; [self.imageView setNeedsDisplay]; } - (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didEndWithForce:(CGFloat)force maxForce:(CGFloat)maxForce { self.imageView.transform = CGAffineTransformIdentity; [self.imageView setNeedsDisplay]; } - (void)forceTouchDidTimeout:(DFContinuousForceTouchGestureRecognizer *)recognizer { self.imageView.transform = CGAffineTransformIdentity; [self.imageView setNeedsDisplay]; } Note that this will only be useful on a device that supports force touch. Also you should not add the DFContinuousForceTouchGestureRecognizer to a view if are you running on iOS 8 or under since it uses the new force property on UITouch only available in iOS 9. If you add this on iOS 8 it will crash, so conditionally add this recognizer based on what iOS version you are running on if you are supporting versions older than iOS 9.
How to check if an SKSpriteNode has been tapped
I have been trying to make a simple app where when you tap a circle, it will disappear, and a new one will come up somewhere else. Here is all my code, edited, but it still does not work. #import "GameScene.h" #implementation GameScene -(void)didMoveToView:(SKView *)view { [self LabelShow]; } int Count = 0; int CountCheck = 0; -(void) LabelShow { //Nothing yet } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { while (CountCheck == Count) { int FNum1 = 350; int TNum1 = 650; int newx = (arc4random()%(TNum1-FNum1))+FNum1; NSLog(#"RandomXCoord: %i", newx); int FNum2 = 100; int TNum2 = 700; int newy = (arc4random()%(TNum2-FNum2))+FNum2; NSLog(#"RandomYCoord: %i", newy); //Location to newx and newy CGPoint location = CGPointMake(newx, newy); SKSpriteNode *Circle = [SKSpriteNode spriteNodeWithImageNamed:#"Circle.png"]; Circle.xScale = 0.2; Circle.yScale = 0.2; Circle.position = location; //Shows chircle [self addChild:Circle]; CountCheck++; for (UITouch *touch in touches) { CGPoint locationTouch = [touch locationInNode:self]; if ([Circle containsPoint:locationTouch]) { NSLog(#"Success!"); [Circle runAction:[SKAction fadeOutWithDuration:0]]; Count++; } } } } #end As I stated, I have the code that puts the circle on the screen in another method. But whenever I run this code (click the circle), the if statement at the bottom does not get executed. I tried all the things in this thread: Can't tap SKSpriteNode - no touch detected ios , but I can't get it to work still, and have changed the code back to the original. Could anyone tell me how I could be able to get the if statement executed when you tap the SKSpriteNode? Thank you for reading this question. Edit I changed the code, but it still doesn't work
Add the following above your Circle.xScale = 0.2; statement Circle.name = #"Circle"; and replace your for-loop with the following for (UITouch *touch in touches) { CGPoint location = [touch locationInNode:self]; SKNode *node = [self nodeAtPoint:location]; if ([node.name isEqualToString:#"Circle"]) { NSLog(#"Success!"); [node removeFromParent]; ++Count; } }
What you need is containsPoint method. Here is the touchesBeganWithEvent: method you are looking for: - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { CGPoint location = [touch locationInNode:self]; if ([circle containsPoint:location] { // executed when you touch a circle } } } Note that you can add more statements and conditions.
I created a game for myself, and I had to do the same thing for a character in it. I subclassed SKNode, and added the character details (SKSpriteNode, name, SKAction, etc) in the subclassed model. Then I added the UITouchBegin method to the subclass, so my character would listen to touches. It is a lot easier to use subclassing, as you can leave all the legwork to the subclass, and focus on the rest of your game. Adding this code to your subclass will take care of it all: -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self removeFromParent]; } You can even set your scene as your character class's delegate, and as soon as the character is removed from parentview, you can create a new one. If you get stuck on the subclass issues, this answer really helped me out. Let me know if any questions and good luck :)
The other responses seem to detect touches on a SKSpriteNode, not a tap. This code detects tap events. Edit TapMaxDelta to change how much movement is allowed before cancelling the tap gesture. class TapNode : SKSpriteNode { // Tap Vars var firstPoint : CGPoint? var TapMaxDelta = CGFloat(10) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } init() { let texture = SKTexture(imageNamed: "Test.png") super.init(texture: texture, color: UIColor.clear, size: texture.size()) isUserInteractionEnabled = true } // ================================================================================================ // Touch Functions // ================================================================================================ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { if let firstTouch = touches.first { firstPoint = firstTouch.location(in: self) } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {} override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let firstTouch = touches.first, let firstPoint = firstPoint { let curPoint = firstTouch.location(in: self) if abs(curPoint.x - firstPoint.x) <= TapMaxDelta && abs(curPoint.y - firstPoint.y) <= TapMaxDelta { print("tap yo") } } } }
UIPanGestureRecognizer - Only vertical or horizontal
I have a view that has a UIPanGestureRecognizer to drag the view vertically. So in the recognizer callback, I only update the y-coordinate to move it. The superview of this view, has a UIPanGestureRecognizer that will drag the view horizontally, just updating the x-coordinate. The problem is that the first UIPanGestureRecognizer is taking the event to move the view vertically, so I can not use the superview gesture. I have tried - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *)otherGestureRecognizer; and both will work, but I don't want that. I want the horizontally to be detected only if the movement is clearly horizontal. So it would be great if the UIPanGestureRecognizer had a direction property. How can I achieve this behavior? I find the docs very confusing, so maybe someone can explain it better here.
Just do this for the vertical pan gesture recognizer, it works for me: - (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer { CGPoint velocity = [panGestureRecognizer velocityInView:someView]; return fabs(velocity.y) > fabs(velocity.x); } And for Swift: func gestureRecognizerShouldBegin(_ gestureRecognizer: UIPanGestureRecognizer) -> Bool { let velocity = gestureRecognizer.velocity(in: someView) return abs(velocity.y) > abs(velocity.x) }
I created a solution with subclassing like in the answer #LocoMike provided, but used the more effective detection mechanism via initial velocity as provided by #Hejazi. I'm also using Swift, but this should be easy to translate to Obj-C if desired. Advantages over other solutions: Simpler and more concise than other subclassing solutions. No additional state to manage. Direction detection happens prior to sending Began action, so your pan gesture selector receives no messages if the wrong direction is swiped. After initial direction is determined, direction logic is no longer consulted. This results in the generally desired behavior of activating your recognizer if the initial direction is correct, but does not cancel the gesture after it has begun if a user's finger doesn't travel perfectly along the direction. Here's the code: import UIKit.UIGestureRecognizerSubclass enum PanDirection { case vertical case horizontal } class PanDirectionGestureRecognizer: UIPanGestureRecognizer { let direction: PanDirection init(direction: PanDirection, target: AnyObject, action: Selector) { self.direction = direction super.init(target: target, action: action) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) if state == .began { let vel = velocity(in: view) switch direction { case .horizontal where fabs(vel.y) > fabs(vel.x): state = .cancelled case .vertical where fabs(vel.x) > fabs(vel.y): state = .cancelled default: break } } } } Example of usage: let panGestureRecognizer = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(handlePanGesture(_:))) panGestureRecognizer.cancelsTouchesInView = false self.view.addGestureRecognizer(panGestureRecognizer) func handlePanGesture(_ pan: UIPanGestureRecognizer) { let percent = max(pan.translation(in: view).x, 0) / view.frame.width switch pan.state { case .began: ... }
I figured it out creating a subclass of UIPanGestureRecognizer DirectionPanGestureRecognizer: #import <Foundation/Foundation.h> #import <UIKit/UIGestureRecognizerSubclass.h> typedef enum { DirectionPangestureRecognizerVertical, DirectionPanGestureRecognizerHorizontal } DirectionPangestureRecognizerDirection; #interface DirectionPanGestureRecognizer : UIPanGestureRecognizer { BOOL _drag; int _moveX; int _moveY; DirectionPangestureRecognizerDirection _direction; } #property (nonatomic, assign) DirectionPangestureRecognizerDirection direction; #end DirectionPanGestureRecognizer.m: #import "DirectionPanGestureRecognizer.h" int const static kDirectionPanThreshold = 5; #implementation DirectionPanGestureRecognizer #synthesize direction = _direction; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; if (self.state == UIGestureRecognizerStateFailed) return; CGPoint nowPoint = [[touches anyObject] locationInView:self.view]; CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view]; _moveX += prevPoint.x - nowPoint.x; _moveY += prevPoint.y - nowPoint.y; if (!_drag) { if (abs(_moveX) > kDirectionPanThreshold) { if (_direction == DirectionPangestureRecognizerVertical) { self.state = UIGestureRecognizerStateFailed; }else { _drag = YES; } }else if (abs(_moveY) > kDirectionPanThreshold) { if (_direction == DirectionPanGestureRecognizerHorizontal) { self.state = UIGestureRecognizerStateFailed; }else { _drag = YES; } } } } - (void)reset { [super reset]; _drag = NO; _moveX = 0; _moveY = 0; } #end This will only trigger the gesture if the user starts dragging in the selected behavior. Set the direction property to a correct value and you are all set.
I tried to constrain the valid area horizontally with UIPanGestureRecognizer. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer; CGPoint velocity = [panGesture velocityInView:panGesture.view]; double radian = atan(velocity.y/velocity.x); double degree = radian * 180 / M_PI; double thresholdAngle = 20.0; if (fabs(degree) > thresholdAngle) { return NO; } } return YES; } Then, only swiping within thresholdAngle degree horizontally can trigger this pan gesture.
Swift 3.0 answer: just handles does the vertical gesture override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let pan = gestureRecognizer as? UIPanGestureRecognizer { let velocity = pan.velocity(in: self) return fabs(velocity.y) > fabs(velocity.x) } return true }
The following solution solved my problem: - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if ([gestureRecognizer.view isEqual:self.view] && [otherGestureRecognizer.view isEqual:self.tableView]) { return NO; } return YES; } This is actually just check if pan is going on main view or tableView.
Swift 3 version of Lee's answer for the lazy import UIKit import UIKit.UIGestureRecognizerSubclass enum PanDirection { case vertical case horizontal } class UIPanDirectionGestureRecognizer: UIPanGestureRecognizer { let direction : PanDirection init(direction: PanDirection, target: AnyObject, action: Selector) { self.direction = direction super.init(target: target, action: action) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) if state == .began { let vel = velocity(in: self.view!) switch direction { case .horizontal where fabs(vel.y) > fabs(vel.x): state = .cancelled case .vertical where fabs(vel.x) > fabs(vel.y): state = .cancelled default: break } } } }
Here is a custom pan gesture in Swift 5 U can constraint its direction and the max angle in the direction, you can also constraint its minimum speed in the direction. enum PanDirection { case vertical case horizontal } struct Constaint { let maxAngle: Double let minSpeed: CGFloat static let `default` = Constaint(maxAngle: 50, minSpeed: 50) } class PanDirectionGestureRecognizer: UIPanGestureRecognizer { let direction: PanDirection let constraint: Constaint init(direction orientation: PanDirection, target: AnyObject, action: Selector, constraint limits: Constaint = Constaint.default) { direction = orientation constraint = limits super.init(target: target, action: action) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) let tangent = tan(constraint.maxAngle * Double.pi / 180) if state == .began { let vel = velocity(in: view) switch direction { case .horizontal where abs(vel.y)/abs(vel.x) > CGFloat(tangent) || abs(vel.x) < constraint.minSpeed: state = .cancelled case .vertical where abs(vel.x)/abs(vel.y) > CGFloat(tangent) || abs(vel.y) < constraint.minSpeed: state = .cancelled default: break } } } } call like this: let pan = PanDirectionGestureRecognizer(direction: .vertical, target: self, action: #selector(self.push(_:))) view.addGestureRecognizer(pan) #objc func push(_ gesture: UIPanGestureRecognizer){ if gesture.state == .began{ // command for once } } or let pan = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(self.push(_:)), constraint: Constaint(maxAngle: 5, minSpeed: 80)) view.addGestureRecognizer(pan)
I took Lee Goodrich's answer and extended it as I needed specifically a single direction pan. Use it like this: let pan = PanDirectionGestureRecognizer(direction: .vertical(.up), target: self, action: #selector(handleCellPan(_:))) I also added some commenting to make it a little clearer what decisions are actually being made. import UIKit.UIGestureRecognizerSubclass enum PanVerticalDirection { case either case up case down } enum PanHorizontalDirection { case either case left case right } enum PanDirection { case vertical(PanVerticalDirection) case horizontal(PanHorizontalDirection) } class PanDirectionGestureRecognizer: UIPanGestureRecognizer { let direction: PanDirection init(direction: PanDirection, target: AnyObject, action: Selector) { self.direction = direction super.init(target: target, action: action) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) if state == .began { let vel = velocity(in: view) switch direction { // expecting horizontal but moving vertical, cancel case .horizontal(_) where fabs(vel.y) > fabs(vel.x): state = .cancelled // expecting vertical but moving horizontal, cancel case .vertical(_) where fabs(vel.x) > fabs(vel.y): state = .cancelled // expecting horizontal and moving horizontal case .horizontal(let hDirection): switch hDirection { // expecting left but moving right, cancel case .left where vel.x > 0: state = .cancelled // expecting right but moving left, cancel case .right where vel.x < 0: state = .cancelled default: break } // expecting vertical and moving vertical case .vertical(let vDirection): switch vDirection { // expecting up but moving down, cancel case .up where vel.y > 0: state = .cancelled // expecting down but moving up, cancel case .down where vel.y < 0: state = .cancelled default: break } } } } }
Swift 4.2 The solution is just for only support pan gesture vertically, same as horizontal. let pan = UIPanGestureRecognizer(target: self, action: #selector(test1)) pan.cancelsTouchesInView = false panView.addGestureRecognizer(pan) Solution 1: #objc func panAction(pan: UIPanGestureRecognizer) { let velocity = pan.velocity(in: panView) guard abs(velocity.y) > abs(velocity.x) else { return } } Solution 2: [UISwipeGestureRecognizer.Direction.left, .right].forEach { direction in let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction)) swipe.direction = direction panView.addGestureRecognizer(swipe) pan.require(toFail: swipe) } Then the swipe gesture will swallow the pan gesture. Of course, you don't need to do anything in swipeAction.
You can find the direction dragging on UIView through UIPanGestureRecognizer. Please follow the code. - (void)viewDidLoad { [super viewDidLoad]; flipFoward = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(doFlipForward:)]; [flipFoward setMaximumNumberOfTouches:1]; [flipFoward setMinimumNumberOfTouches:1]; [flipFoward setDelegate:self]; [self.view addGestureRecognizer:flipFoward]; flipBack = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(doFlipBack:)]; [flipBack setMaximumNumberOfTouches:1]; [flipBack setMinimumNumberOfTouches:1]; [flipBack setDelegate:self]; [self.view addGestureRecognizer:flipBack]; } #pragma mark - #pragma mark RESPONDER -(void)doFlipForward:(UIGestureRecognizer *)aGestureRecognizer{ NSLog(#"doFlipForward"); if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateBegan) { NSLog(#"UIGestureRecognizerStateBegan"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateChanged) { NSLog(#"UIGestureRecognizerStateChanged"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateEnded) { NSLog(#"UIGestureRecognizerStateEnded"); } } -(void)doFlipBack:(UIGestureRecognizer *)aGestureRecognizer{ NSLog(#"doFlipBack"); if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateBegan) { NSLog(#"UIGestureRecognizerStateBegan1"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateChanged) { NSLog(#"UIGestureRecognizerStateChanged1"); } if([(UIPanGestureRecognizer*)aGestureRecognizer state] == UIGestureRecognizerStateEnded) { NSLog(#"UIGestureRecognizerStateEnded1"); } } #pragma mark - #pragma mark DELEGATE -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ CGSize size = [self.view bounds].size; CGFloat touchX = [gestureRecognizer locationInView:self.view].x; if((gestureRecognizer == flipFoward) && touchX >= (size.width - 88.0f)) { return YES; } if((gestureRecognizer == flipBack) && touchX <= 88.0f) { return YES; } return NO; }
Here is how I resolved: First I enabled Simultaneously PanGesture Recognition. -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; Then I Isolate Horizontal and Vertical Pan gestures (accumulator is NSMutableArray property): - (void)verticalPan :(UIPanGestureRecognizer *) sender { CGPoint touch = [sender translationInView:self]; NSValue *value = [NSValue valueWithCGPoint:touch]; [accumulator addObject:value]; int firstXObjectValue = (int)[[accumulator objectAtIndex:0] CGPointValue].x ; int lastXObjectValue = (int)[[accumulator lastObject] CGPointValue].x; int firstYObjectValue = (int)[[accumulator objectAtIndex:0] CGPointValue].y; int lastYObjectValue = (int)[[accumulator lastObject] CGPointValue].y; if (abs(lastYObjectValue - firstYObjectValue) < 4 && abs(lastXObjectValue - firstXObjectValue) > 4) { NSLog(#"Horizontal Pan"); //do something here } else if (abs(lastYObjectValue - firstYObjectValue) > 4 && abs(lastXObjectValue - firstXObjectValue) < 4){ NSLog(#"Vertical Pan"); //do something here } if (accumulator.count > 3) [accumulator removeAllObjects]; I pushed an example here: add custom pan in scrollview
let pangesture = UIPanGestureRecognizer(target: self, action: "dragview:") yourview.addGestureRecognizer(pangesture) func dragview(panGestureRecognizer:UIPanGestureRecognizer) { let touchlocation = panGestureRecognizer.locationInView(parentview) yourview.center.y = touchlocation.y //x for horizontal }
You may use simple panGestureRecognizer. No need to use pandirectionregognizer or stuff. Just use y value of translationInview Below code move drag view only up and down - (void)gesturePan_Handle:(UIPanGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateChanged) { CGPoint translation = [gesture translationInView:gesture.view]; recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y); [gesture setTranslation:CGPointMake(0, 0) inView:gesture.view]; } }
- (void)dragAction:(UIPanGestureRecognizer *)gesture{ UILabel *label = (UILabel *)gesture.view; CGPoint translation = [gesture translationInView:label]; label.center = CGPointMake(label.center.x + translation.x, label.center.y + 0); [gesture setTranslation:CGPointZero inView:label];} I created PanGestureRecognizer #selector action method for the object that needed only Horizontal scrolling. UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(smileyDragged:)]; [buttonObject addGestureRecognizer:gesture];
Swift way override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer { return isVerticalGesture(panGestureRecognizer) } return false } private func isVerticalGesture(_ recognizer: UIPanGestureRecognizer) -> Bool { let translation = recognizer.translation(in: superview!) if fabs(translation.y) > fabs(translation.x) { return true } return false }
For all you Swift users out there, this will do the job :) import Foundation import UIKit.UIGestureRecognizerSubclass class DirectionPanGestureRecognizer: UIPanGestureRecognizer { let kDirectionPanThreshold = CGFloat(5) var drag = true var moveX = CGFloat(0) var moveY = CGFloat(0) override init(target: AnyObject, action: Selector) { super.init(target: target, action: action) } override func touchesMoved(touches: NSSet, withEvent event: UIEvent) { super.touchesMoved(touches, withEvent: event) if state == .Failed { return } let nowPoint = touches.anyObject()?.locationInView(view) let prevPoint = touches.anyObject()?.previousLocationInView(view) moveX += prevPoint!.x - nowPoint!.x moveY += prevPoint!.y - nowPoint!.y if !drag { if abs(moveX) > kDirectionPanThreshold { state = .Failed } else { drag = true } } } override func reset() { super.reset() moveX = 0 moveY = 0 drag = false } }
I took an excellent answer by Lee Goodrich and ported to Swift 3 import UIKit import UIKit.UIGestureRecognizerSubclass enum PanDirection { case vertical case horizontal } class PanDirectionGestureRecognizer: UIPanGestureRecognizer { let direction : PanDirection init(direction: PanDirection, target: AnyObject, action: Selector) { self.direction = direction super.init(target: target, action: action) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) if state == .began { let vel = velocity(in: self.view!) switch direction { case .horizontal where fabs(vel.y) > fabs(vel.x): state = .cancelled case .vertical where fabs(vel.x) > fabs(vel.y): state = .cancelled default: break } } } }
I would love to share my approach because all other approaches are based on either UIGestureRecognizerDelegate or subclassing UIPanGestureRecognizer. My approach is based on runtime and swizzling. I'm not 100% sure about this approach, but you can test and improve it yourself. Set the direction of any UIPanGestureRecognizer with just one line of code: UITableView().panGestureRecognizer.direction = UIPanGestureRecognizer.Direction.vertical use pod 'UIPanGestureRecognizerDirection' or the code: public extension UIPanGestureRecognizer { override open class func initialize() { super.initialize() guard self === UIPanGestureRecognizer.self else { return } func replace(_ method: Selector, with anotherMethod: Selector, for clаss: AnyClass) { let original = class_getInstanceMethod(clаss, method) let swizzled = class_getInstanceMethod(clаss, anotherMethod) switch class_addMethod(clаss, method, method_getImplementation(swizzled), method_getTypeEncoding(swizzled)) { case true: class_replaceMethod(clаss, anotherMethod, method_getImplementation(original), method_getTypeEncoding(original)) case false: method_exchangeImplementations(original, swizzled) } } let selector1 = #selector(UIPanGestureRecognizer.touchesBegan(_:with:)) let selector2 = #selector(UIPanGestureRecognizer.swizzling_touchesBegan(_:with:)) replace(selector1, with: selector2, for: self) let selector3 = #selector(UIPanGestureRecognizer.touchesMoved(_:with:)) let selector4 = #selector(UIPanGestureRecognizer.swizzling_touchesMoved(_:with:)) replace(selector3, with: selector4, for: self) } #objc private func swizzling_touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { self.swizzling_touchesBegan(touches, with: event) guard direction != nil else { return } touchesBegan = true } #objc private func swizzling_touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { self.swizzling_touchesMoved(touches, with: event) guard let direction = direction, touchesBegan == true else { return } defer { touchesBegan = false } let forbiddenDirectionsCount = touches .flatMap({ ($0.location(in: $0.view) - $0.previousLocation(in: $0.view)).direction }) .filter({ $0 != direction }) .count if forbiddenDirectionsCount > 0 { state = .failed } } } public extension UIPanGestureRecognizer { public enum Direction: Int { case horizontal = 0 case vertical } private struct UIPanGestureRecognizerRuntimeKeys { static var directions = "\(#file)+\(#line)" static var touchesBegan = "\(#file)+\(#line)" } public var direction: UIPanGestureRecognizer.Direction? { get { let object = objc_getAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.directions) return object as? UIPanGestureRecognizer.Direction } set { let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC objc_setAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.directions, newValue, policy) } } fileprivate var touchesBegan: Bool { get { let object = objc_getAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.touchesBegan) return (object as? Bool) ?? false } set { let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC objc_setAssociatedObject(self, &UIPanGestureRecognizerRuntimeKeys.touchesBegan, newValue, policy) } } } fileprivate extension CGPoint { var direction: UIPanGestureRecognizer.Direction? { guard self != .zero else { return nil } switch fabs(x) > fabs(y) { case true: return .horizontal case false: return .vertical } } static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } }
I tried this: which worked for me as per the question describes func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer { return true } else { return false } }
SWIFT 4.2 I went further and make a direction Pan Gesture: enum PanDirection { case up case left case right case down } class PanDirectionGestureRecognizer: UIPanGestureRecognizer { fileprivate let direction: PanDirection init(direction: PanDirection, target: AnyObject, action: Selector) { self.direction = direction super.init(target: target, action: action) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) guard state != .failed else { return } let vel = velocity(in: view) let velocities: [PanDirection: CGFloat] = [.up: -vel.y, .left: -vel.x, .right: vel.x, .down: vel.y] let sortedKeys = velocities.sorted { $0.1 < $1.1 } if let key = sortedKeys.last?.key, key != direction { state = .cancelled } } } (Used: https://github.com/fastred/SloppySwiper and https://stackoverflow.com/a/30607392/5790492)
PanGestureRecognizer interface contains the following definitions: unsigned int _canPanHorizontally:1; unsigned int _canPanVertically:1; I didn't check this, but maybe it's accessible via subclass.