SpriteKit: add UIGestureRecognizer and detect which node was swiped - ios

In which method do I add a UIGestureRecognizer to my SKScene. And how do I detect which node was swiped? This doesn't seem to work:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
...
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
recognizer.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizer];
}
return self;
}

You add the UISwipeGestureRecognizer in this method:
- (void)didMoveToView:(SKView *)view
{
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
recognizer.direction = UISwipeGestureRecognizerDirectionUp;
[[self view] addGestureRecognizer:recognizer];
}
And that's how you detect which SKNode was swiped:
- (void)handleSwipe:(UISwipeGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint touchLocation = [sender locationInView:sender.view];
touchLocation = [self convertPointFromView:touchLocation];
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
NSLog(#"%#", touchedNode);
}
}

Swift 3 version inspired by S.E.'s answer:
func didSceneViewPan(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
let touchPoint = sender.location(in: sender.view)
let touchLocation = convertPoint(fromView: touchPoint)
if isPointInNode(point: touchLocation, node: testNode) {
print("yes, node touched!")
} else {
print("no bueno :(")
}
}
}
fileprivate func isPointInNode(point: CGPoint, node: SKNode) -> Bool {
// Get all nodes intersecting <point>
let nodes = self.nodes(at: point)
// Return true on first one that matches <node>
for n in nodes {
if n == node {
return true
}
}
// If here, <point> not inside <node> so return false
return false
}

For 2017...
Say you have a scene with various sprites.
Tap on one of them. You need to know which one was tapped ...
class YourScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
// your setup for the scene - example, add objects etc
setupTapDetection()
}
func setupTapDetection() {
let t = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
view?.addGestureRecognizer(t)
}
#objc func tapped(_ tap: UITapGestureRecognizer) {
// critical...
if tap.state != .ended { return }
// Note: you actually do NOT "invert the Y axis",
// these two calls in fact take care of that properly.
let viewPoint = tap.location(in: tap.view)
let scenePoint = convertPoint(fromView: viewPoint)
// technically there could be more than one node...
let nn = nodes(at: scenePoint)
if nn.count == 0 {
print("you very like tapped 'nowhere'")
return
}
if nn.count != 1 {
// in almost all games or physics scenes,
// it is not possible this would happen
print("something odd happened - overlapping nodes?")
return
}
// you must have a custom class for your sprites
// Say it is class YourSprites: SKSpriteNode
if let dotSpriteTapped = nn.first! as? YourSprites {
// we are done. dotSpriteTapped is the YourSprite,
// which was tapped. now do whatever you want.
let nm = String(describing: dotSpriteTapped.name)
print(" \(nm)")
// you can now access your own properties:
let whichTank = dotSpriteTapped.someID
print(" \(whichTank)")
let tankPower = dotSpriteTapped.sheildStrength
print(" \(tankPower)")
}
}
Enjoy

You can check of the nodes at a location include a node you're looking for using this:
nodes(at: touchLocation).contains(nodeYouAreLookingFor)

Related

how can I detect if singleTap was tapped or annotation on mapView of mapbox?

Requirment on iOS for mapBox map. (I am not talking about MKMapView)
how can we detect if singleTap was tapped or annotation on mapView? I need that singleTap will be handled only on empty area of map (without pins), and didSelectAnnotation called when i tap on pin.
But I found on android we have method like this
mapboxMap.setOnMapClickListener(new MapboxMap.OnMapClickListener() {
public void onMapClick(#NonNull LatLng point) {
Toast.makeText(getActivity(),"on Tap "+point.getLatitude(),Toast.LENGTH_LONG).show();
}
});
and along with that
mapboxMap.setInfoWindowAdapter(new MapboxMap.InfoWindowAdapter() { ... }) will display the annotation.
Don't we have same kind of concept in iOS ?
The actual problem is in iOS is, when I add singleTapGesture on mapView of Mapbox
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
[self.mapView addGestureRecognizer:singleTapGesture];
the delegate method of mapView of mapbox will not call.
- (nullable UIView <MGLCalloutView> *)mapView:(MGLMapView *)mapView calloutViewForAnnotation:(id <MGLAnnotation>)annotation;
to make sure the delegate method must call, then I dont have to use singleTapGesture
Here the situation is either this or that, but as per me need I needed both.
Looking forward of any solution.
Thanks,
Subclass MGLMapView and delegate its' touchesEnded.
protocol MapViewTapDelegate: class {
func mapViewTapped(withTouchLocation touchLocation: CLLocationCoordinate2D)
}
class MapView: MGLMapView {
weak var tapDelegate: MapViewTapDelegate?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
let touchLocation = convert(touch.location(in: self), toCoordinateFrom: nil)
tapDelegate?.mapViewTapped(withTouchLocation: touchLocation)
}
}
As touchesEnded isn't triggered when didSelect annotation is called and vice versa, this is what you need.
class ViewController: UIViewController {
#IBOutlet weak var mapView: MapView! {
didSet {
mapView.delegate = self; mapView.tapDelegate = self
}
}
}
extension ViewController: MGLMapViewDelegate {
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
print("didSelect annotationWithCoordinate: \(annotation.coordinate)")
}
}
extension ViewController: MapViewTapDelegate {
func mapViewTapped(withTouchLocation touchLocation: CLLocationCoordinate2D) {
print("mapViewTapped touchLocation: \(touchLocation)")
}
}
I think that the method
-(void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id<MGLAnnotation>)annotation will solve your problem for selecting annotation.
I made another workaround here
I cloned the release-ios-v3.3.0 and created package using Building the SDK and added one delegate method in
MGLMapvViewDelegate.h
as per my need something like that.
-(void)mapView:(MGLMapView *)mapView tapOnNonAnnotationAreaWithPoints:(CGPoint)points
and in MGLMapView.mm I updated the code like that,
else{
if(self.selectedAnnotation)
[self deselectAnnotation:self.selectedAnnotation animated:YES];
else if([self.delegate respondsToSelector:#selector(mapView:tapOnNonAnnotationAreaWithPoints:)])
[self.delegate mapView:self tapOnNonAnnotationAreaWithPoints:tapPoint];
}
which is in -(void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap
method.
Its working for me, as I am able to detect single tap on non annotation area. later I convert the passed points to geo coordinates, so that I work with newly tapped coordinate.
Note
Newly Created library size is about 48.7 MB where as downloaded library was 38.3 MB.
Style is not working properly, you can see in attached pic. (Tried with different Style URL but none of it is same as previous)
I feel lack in App performance, Zooming and Panning is not smooth as the dynamic library gave by Mapbox guys.
I am still exploring. lemme know if you wanna me to check/explore.
Please accept this answer, if it is helpful.
Thank you,
Happy Coding.
Try swift 3
//determined is Point inner to realm polygons
func determinedInPoligon(point:CLLocationCoordinate2D, poligon:[CLLocationCoordinate2D]) -> Bool {
var result:Bool = false
var j = poligon.count - 1
for i in 0 ..< poligon.count {
if (poligon[i].longitude < point.longitude && poligon[j].longitude >= point.longitude || poligon[j].longitude < point.longitude && poligon[i].longitude >= point.longitude) &&
(poligon[i].latitude + (point.longitude - poligon[i].longitude) / (poligon[j].longitude - poligon[i].longitude) * (poligon[j].latitude - poligon[i].latitude) < point.latitude) {
result = !result;
}
j = i;
}
return result
}
func squareFrom(location: CGPoint, radius: Double) -> CGRect {//return rect with CGPoint center and radius
let length = radius
return CGRect(x: Double(location.x - CGFloat(length / 2)), y:
Double(location.y - CGFloat(length / 2)), width: length, height: length)
}
function handle
func handleTap(_ gesture: UITapGestureRecognizer) {
// Get the CGPoint where the user tapped.
let spot = gesture.location(in: mapView)
let my_features = mapView.visibleFeatures(at: spot)
let strZoomValue = mapView.zoomLevel > 15 ? "6" : "4"
//the feature structur object value not equal annotation object
for value in my_features.enumerated() {// feature items
if value.element is MGLPointAnnotation {
for annot in (mapView.annotations?.enumerated())! { // annotation items
if annot.element is MGLPointAnnotation {
//rounded lat and lng value
var arr_cllocation = [CLLocationCoordinate2D]()
for cllocation in [(annot.element as! MGLPointAnnotation).coordinate, (value.element as! MGLPointAnnotation).coordinate] {
let strLat = String(format: "%."+strZoomValue+"f", cllocation.latitude)
let strLon = String(format: "%."+strZoomValue+"f", cllocation.longitude)
arr_cllocation.append(
CLLocationCoordinate2D(latitude: CLLocationDegrees(strLat)!, longitude: CLLocationDegrees(strLon)!) )
}
if arr_cllocation.count == 2 &&
memcmp(&arr_cllocation[0], &arr_cllocation[1], MemoryLayout<CLLocationCoordinate2D>.size) == 0 {// 0 if equal object
// to do custom popup view
let instViewPopupLineClass = UINib(nibName: "ViewPopupBase", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView
for objectInst in instViewPopupLineClass.subviews.enumerated() {
if objectInst.element is UILabel {
let asdasdas = (annot.element as! MGLPointAnnotation).subtitle
(objectInst.element as! UILabel).text = asdasdas
MyCustomPopup(customView: instViewPopupLineClass, positionXY: spot)
break
}
}
}
}
} //for end
}
}
}
OR not accurate method, but a worker ;)
func handleTap(_ gesture: UITapGestureRecognizer) {// Get the CGPoint where the user tapped.
let spot = gesture.location(in: mapView)
let cllcordinatTap = mapView.convert(spot, toCoordinateFrom: mapView)
//for determined zoom scala
let deltScalaMap = abs(self.mapView.maximumZoomLevel - self.mapView.zoomLevel)
//The large is zoom maps, then smal size is tap location, and vice versa.
let checkScalaMap = deltScalaMap == 0 ? 1 : deltScalaMap
let _rect = squareFrom(location: CGPoint(x: cllcordinatTap.latitude, y: cllcordinatTap.longitude), radius: 0.00005 * checkScalaMap)
for annot in (mapView.annotations?.enumerated())! {
if annot.element is MGLPointAnnotation {
let _cordinatCurrentAnnotation = (annot.element as! MGLPointAnnotation).coordinate
if determinedInPoligon(point: _cordinatCurrentAnnotation, poligon:
[CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.minX), longitude: CLLocationDegrees(_rect.minY)),
CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.maxX), longitude: CLLocationDegrees(_rect.minY)),
CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.maxX), longitude: CLLocationDegrees(_rect.maxY)),
CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.minX), longitude: CLLocationDegrees(_rect.maxY))
]) {
// to do, if tap MGLPointAnnotation, annot.element
}
}
}
}
Here I got some workaround which helped to work with my requirement.
As per my need I am able to get single tap detection on both mapbox annotation marker and as well as on empty area of map
I created category for MGLMapView, (MGLMapView+EDCMapboxView) and overrides the touch methods
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
MGLMapView+EDCMapboxView.h
#protocol EDCMapboxViewDelegate <NSObject>
#optional
- (void)mapboxViewDidCreateNewTicket:(MGLMapView*)mapView;
#end
#interface MGLMapView (EDCMapboxView)
#property (assign, nonatomic) BOOL shouldCreateNewTicket;
#property (weak, nonatomic) id <EDCMapboxViewDelegate> mapboxDelegate;
#end
MGLMapView+EDCMapboxView.m
#implementation MGLMapView (EDCMapboxView)
#dynamic mapboxDelegate;
#pragma mark -- Accessor
- (BOOL)shouldCreateNewTicket {
return [objc_getAssociatedObject(self, #selector(shouldCreateNewTicket)) boolValue];
}
- (void)setShouldCreateNewTicket:(BOOL)flag {
objc_setAssociatedObject(self, #selector(shouldCreateNewTicket), #(flag), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id<EDCMapboxViewDelegate>)mapboxDelegate{
return objc_getAssociatedObject(self, #selector(mapboxDelegate));
}
- (void)setMapboxDelegate:(id<EDCMapboxViewDelegate>)mapboxDelegate{
objc_setAssociatedObject(self, #selector(mapboxDelegate), mapboxDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark -- Overrided method for UIResponder
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesBegan");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesMoved");
self.shouldCreateNewTicket = NO;
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesEnded");
}
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
NSLog(#"touchesCancelled");
[self createNewTicket];
}
- (void)createNewTicket{
if(self.shouldCreateNewTicket){
NSLog(#"Allowed to Create New ticket");
// Tells that tap is on empty area.
if([self.mapboxDelegate respondsToSelector:#selector(mapboxViewDidCreateNewTicket:)]){
[self.mapboxDelegate mapboxViewDidCreateNewTicket:self];
}
}
else{
NSLog(#"Not allowed to create New ticket");
// Tells tap is on annotation marker.
self.shouldCreateNewTicket = YES;
}
}
EDCMapboxViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.mapView.shouldCreateNewTicket = YES;
.....
.......
........
}
- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation {
NSLog(#"annotationCanShowCallout");
// Tells that annotation is tapped, then do not allow to create ticket.
self.mapView.shouldCreateNewTicket = NO;
return YES;
}
- (void)mapboxViewDidCreateNewTicket:(MGLMapView*)mapView{
// Tells that tap is on empty area and not on marker, then allow to create ticket.
}
It worked for me, hope it will help you guys as well.
Thanks.
Here is how I solved it, you should get the concept.
func onMapSingleTapped(recognizer: UITapGestureRecognizer)
{
let viewLocation: CGPoint = recognizer.locationInView(map)
// check if any annotations are hit
if(map.annotations != nil)
{
for annotation in map.annotations!
{
if(annotation.isKindOfClass(MapCheckInAnnotation))
{
let pin: MapCheckInAnnotation = annotation as! MapCheckInAnnotation
if let pinView = pin.view
{
print("pinview \(pinView.frame.origin)")
// check if hit pin instead of just map
if(viewLocation.x >= pinView.frame.origin.x && viewLocation.x < pinView.frame.origin.x + pinView.frame.width)
{
if(viewLocation.y >= pinView.frame.origin.y && viewLocation.y < pinView.frame.origin.y + pinView.frame.height)
{
mapView(map, didSelectAnnotationView: pinView)
return
}
}
}
}
}
}
// nope, no annotations were clicked
let mapLocation: CLLocationCoordinate2D = map.convertPoint(viewLocation, toCoordinateFromView: map)
print("onMapSingleTapped \(mapLocation)")
}
I tried sample project for your question and it works well.
.h
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#interface ViewController : UIViewController<MKMapViewDelegate>
#property (strong, nonatomic) IBOutlet MKMapView *mapTapAnnotation;
#end
.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize mapTapAnnotation;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self setMapViewWithAnnnoationPin];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void)setMapViewWithAnnnoationPin
{
mapTapAnnotation.showsUserLocation = YES;
mapTapAnnotation.mapType = MKMapTypeHybrid;
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(12.900988, 80.227930);
MKCoordinateSpan span = MKCoordinateSpanMake(0.005, 0.005);
MKCoordinateRegion region = {coord, span};
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
[annotation setCoordinate:coord];
[annotation setTitle:#"Single Tap"]; //You can set the subtitle too
[mapTapAnnotation setRegion:region];
[mapTapAnnotation addAnnotation:annotation];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(detectSingleTap)];
tapGesture.numberOfTapsRequired = 1;
[mapTapAnnotation addGestureRecognizer:tapGesture];
}
-(void)detectSingleTap
{
NSLog(#"Finded the single tap on map view");
}
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
NSLog(#"The single tap annotation pin point is - %ld",(long)view.tag);
}
The printed output results are
Finded the single tap on map view
The single tap annotation pin point is - 0
As I haven't been successful with the solutions above I implemented a small delay after the always recognized map tap event. If an annotation is selected in this small time frame, then nothing is called any further. Otherwise only the map view was tapped and no annotation.
Works pretty well in my case.
MapView part
protocol MapDelegate: class {
func didTapMapView()
}
class AppleMapView: MKMapView {
weak var mapDelegate: MapDelegate?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if touches.first != nil {
mapDelegate?.didTapMapView()
}
}
........
}
MapViewController part
// property to store last tap at annotation
var lastAnnotationSelectionTime = Date()
// this method is called on every tap
func didTapMapView() {
let overlaySelectionDelayTime: TimeInterval = 0.2
DispatchQueue.main.asyncAfter(deadline: .now() + overlaySelectionDelayTime, execute: { [weak self] in
guard let self = self else { return }
if self.lastAnnotationSelectionTime.distance(to: Date()) > overlaySelectionDelayTime {
// only called if no annotation was tapped meanwhile
[ HANDLE TAP ON MAP HERE ]
}
})
}
// MARK: Marker Selection
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
lastAnnotationSelectionTime = Date() // setten if annotation is tapped
[ HANDLE TAP ON ANNOTATION HERE ]
}

Is there something that will repeat touchesBegan?

I have a button that when getting pressed will move a node. How can I repeat this non stop while button is pressed. I guess I'm looking for something between touchesBegan and touchesEnded. Something like touchesContinue because I'd like the node to continue moving while the button is pressed. This is what I have so far.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// 1
leftMove.name = "Left"
rightMove.name = "Right"
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if (node.name == "Left") {
// Implement your logic for left button touch here:
player.position == CGPoint(x:player.position.x-1, y:player.position.y)
} else if (node.name == "Right") {
// Implement your logic for right button touch here:
player.position = CGPoint(x:player.position.x+1, y:player.position.y)
}
}
Use NSTimer to repeat the move action. Have the timer started on the Touch Down and have the timer stopped on either Touch Up Inside or Touch Up Outside.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var button: UIButton!
var timer: NSTimer!
leftMove.name = "Left"
rightMove.name = "Right"
override func viewDidLoad() {
super.viewDidLoad()
button = UIButton(frame: CGRectMake(100, 100, 50, 50)) as UIButton
//add Target for .TouchDown events, when user touch down
button.addTarget(self, action: #selector(ViewController.touchDown(_:)), forControlEvents: .TouchDown)
//add Target for .TouchUpInside events, when user release the touch
button.addTarget(self, action: #selector(ViewController.release(_:)), forControlEvents: .TouchUpInside)
view.addSubview(button)
}
func touchDown(sender: UIButton) {
//start the timer
timer = NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: #selector(ViewController.movePlayer(_:)), userInfo: nil, repeats: true)
}
func release(sender: UIButton) {
//stop the timer
self.timer.invalidate()
self.timer = nil
}
func movePlayer(sender: NSTimer) {
//move your stuff here
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if (node.name == "Left") {
// Implement your logic for left button touch here:
player.position == CGPoint(x:player.position.x-1, y:player.position.y)
} else if (node.name == "Right") {
// Implement your logic for right button touch here:
player.position = CGPoint(x:player.position.x+1, y:player.position.y)
}
}
}
THIS IS IN OBJECTIVE C
i suggest you to use this i did it with touch down event
create Timer object global
TOUCH UP INSIDE and Drag and EXIT
- (IBAction)arrowUpUpperLipTapped:(UIButton *)sender {
NSLog(#"SIngle TAPPED");
if (self.timer1) {
[self.timer1 invalidate];
self.timer1 = nil;
}
[self arrowUpperLipPosChanged:sender];
}
//-----------------------------------------------------------------------
TOUCH DOWN
- (IBAction)arrowUpperLipLongTapped:(UIButton *)sender {
NSLog(#"LONG TAPPED");
if (self.timer) {
[self.timer1 invalidate];
self.timer1 = nil;
}
_timer1 = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(arrowUpperLipTimerAction:) userInfo:sender repeats:YES];
}
//-----------------------------------------------------------------------
- (void) arrowUpperLipPosChanged :(UIButton *) sender {
CGRect frame = self.viewUpperLip.frame;
if (sender.tag == 9001) {
// UP Arrow Tapped
frame.origin.y --;
} else if (sender.tag == 9002) {
frame.origin.y ++;
}
if (!CGRectContainsPoint(CGRectMake(frame.origin.x, frame.origin.y + frame.size.height / 2, frame.size.width, frame.size.height), self.imgViewGridHEyesCenter.center) && !(CGRectIntersection(frame, [self.view viewWithTag:203].frame).size.height >= frame.size.height)) {
[self.viewUpperLip setFrame:frame];
}
}
//-----------------------------------------------------------------------
- (void) arrowUpperLipTimerAction :(NSTimer *) timer {
UIButton *sender = (UIButton *)[timer userInfo];
[self arrowUpperLipPosChanged:sender];
}

UISwipeGestureRecognizer only one direction working

So Im making a page with pageControl (it's a page with multiple views with dots indicating which page you're in), my code looks like the following in viewDidLoad:
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeAction:)];
UIView *temp = [[UIView alloc]initWithFrame:self.view.frame];
temp.backgroundColor = [UIColor clearColor];
[temp addGestureRecognizer:swipe];
[self.view addSubview:temp];
And in the swipeAction selector I have:
- (void)swipeAction: (UISwipeGestureRecognizer *)sender{
NSLog(#"Swipe action called");
if (sender.direction == UISwipeGestureRecognizerDirectionLeft) {
//Do Something
}
else if (sender.direction == UISwipeGestureRecognizerDirectionRight){
//Do Something Else
}
}
To my surprise, this method only works when you swipe to the right (i.e. the else if block gets called). When you swipe left, the swipeAction doesn't even get called! This is strange, why does this happen and how should I change my code? Any reply is appreciated. Thanks a lot!
There's a couple things you should be aware of here. First, you have to create a gesture for each direction that you want to observe. This isn't a big deal though because you can simple give them the same selector, and it will be just like one gesture for two directions.
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeAction:)];
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipeAction:)];
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
UIView *temp = [[UIView alloc]initWithFrame:self.view.frame];
temp.backgroundColor = [UIColor clearColor];
[temp addGestureRecognizer:leftSwipe];
[temp addGestureRecognizer:rightSwipe];
[self.view addSubview:temp];
Second, you never specified the direction of the gesture leaving it to default to right (or 1 on the direction enum)
From the documentation:
The default direction is UISwipeGestureRecognizerDirectionRight. See descriptions of UISwipeGestureRecognizerDirection constants for more information.
typedef enum {
UISwipeGestureRecognizerDirectionRight = 1 << 0,
UISwipeGestureRecognizerDirectionLeft = 1 << 1,
UISwipeGestureRecognizerDirectionUp = 1 << 2,
UISwipeGestureRecognizerDirectionDown = 1 << 3
} UISwipeGestureRecognizerDirection;
Swift 4
let swiper = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeFunction(sender:)))
swiper.direction = UISwipeGestureRecognizer.Direction.right
let swipel = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeFunction(sender:)))
swipel.direction = UISwipeGestureRecognizer.Direction.left
UIView().addGestureRecognizer(swiper)
UIView().addGestureRecognizer(swipel)
#objc func swipeFunction(sender: UISwipeGestureRecognizer) {
print(sender)
}
you should be able to figure it out from there, you add a UISwipeGestureRecognizer for each direction to the UIView
swipe.direction sets the direction(s) you're recognizing, it doesn't tell you which direction was swiped. Add this line when creating your recognizer:
swipe.direction = UISwipeGestureRecognizerDirectionLeft|UISwipeGestureRecognizerDirectionRight;
If you need to detect which direction was swiped, just use two different Recognizers, one for left and one for right.
I was facing the same issue and found a way by extending the UIPanGestureRecognizer in order to implement a UIDirectionalSwipeGestureRecognizer as follows. The direction of the swipe is available as a public property of the recognizer's instance:
import UIKit
public final class UIDirectionalSwipeGestureRecognizer: UIPanGestureRecognizer {
public private(set) var direction: UISwipeGestureRecognizer.Direction?
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
direction = nil
if let touch = touches.first {
startTimestamp = touch.timestamp
startLocation = touch.location(in: nil)
}
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if
let currentTimestamp = touches.first?.timestamp,
let startTimestamp = startTimestamp,
currentTimestamp - startTimestamp > 0.3
{
touchesCancelled(touches, with: event)
}
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if
let endLocation = touches.first?.location(in: nil),
let startLocation = startLocation,
startLocation.distance(to: endLocation) > 20
{
direction = UISwipeGestureRecognizer.Direction.fromPoint(startLocation, to: endLocation)
}
}
// MARK: - Private
private var startTimestamp: TimeInterval?
private var startLocation: CGPoint?
}
// MARK: - Extensions
public extension CGPoint {
func distanceSquared(to: CGPoint) -> CGFloat {
(to.x - x) * (to.x - x) + (to.y - y) * (to.y - y)
}
func distance(to: CGPoint) -> CGFloat {
sqrt(distanceSquared(to: to))
}
}
extension UISwipeGestureRecognizer.Direction {
public static func fromPoint(_ startPoint: CGPoint, to endPoint: CGPoint) -> Self {
let offset = CGSize(width: endPoint.x - startPoint.x, height: endPoint.y - startPoint.y)
if abs(offset.width) > abs(offset.height) {
return offset.width > 0 ? .right : .left
} else {
return offset.height > 0 ? .up : .down
}
}
}

Setting up buttons in SKScene

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)
}
}

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.

Resources