[Tutorial] Multi touch/Single touch/Tap handling.

Discussion in 'iOS Development' started by SkylarEC, Jan 25, 2009.

  1. SkylarEC

    SkylarEC Super Moderator Emeritus Staff Member

    Joined:
    Sep 19, 2007
    Messages:
    6,642
    Likes Received:
    129
    In response to a recent thread in which no one was seemingly able to help the original poster, I made this quick little application. Hopefully you should be able to learn.

    Please Register or Log in to view images


    • Single Tap on the beach ball - The beach ball will let you know you tapped on it by pulsing. User feedback is ultra important.
    • Drag your finger around the screen to move the beach ball wherever.
    • Using two fingers, spin the beach ball to rotate it on screen.
    • Double tap anywhere on the screen to reset the beach ball's rotation adn location.

    The code is clear, so you should have no problem following it. All my touch events are handled by the view controller. If you still aren't using view controllers, shame on you. The MVC method makes life quite a bit easily, and is HIGHLY recommended by Apple. Also, without using view controllers, you won't be able to use neat things such as Navigation and TabBar controllers (among others).


    TEST_SPINVIEW_APPViewController.h
    [OBJC]#import <UIKit/UIKit.h>

    #define degreesToRadians(x) (M_PI * x / 180.0)

    @interface TEST_SPINVIEW_APPViewController : UIViewController {
    UIImageView *beachBall;
    CGPoint touch1;
    CGPoint touch2;
    }

    @end[/OBJC]

    I'll be the first to admit, I hate working with radians. To make my life easier, I define a function that will convert radians to degrees. I recommend using this in any instance where you need to feed another function radians. Simply put, it will look something like this: degreesToRadians(45); That will convert a 45 degree angle into a radian. Doesn't that make life so much simpler.

    In the view controller, you'll see how I convert radians into degrees. Technically, I don't have to do this. I could leave things as radians only, and feed the radian into the rotation method directly, but degrees are much simpler to debug with. Ultimately, it's your call which method you decide to choose. In an instance where performance isn't an issues, i'd say do what you want. If every cycle counts, then do not convert to degrees, you're just doing needless math.

    TEST_SPINVIEW_APPViewController.m
    [OBJC]#import "TEST_SPINVIEW_APPViewController.h"

    @implementation TEST_SPINVIEW_APPViewController


    - (void)viewDidLoad {
    [super viewDidLoad];

    [self.view setMultipleTouchEnabled:YES];
    self.view.backgroundColor = [UIColor whiteColor];

    beachBall = [[UIImageView alloc] initWithImage:[UIImage imageNamed

    Please Register or Log in to view images

    "ball.png"]];
    beachBall.center = self.view.center;

    [self.view addSubview:beachBall];
    }


    - (BOOL)shouldAutorotateToInterfaceOrientation

    Please Register or Log in to view images

    UIInterfaceOrientation)interfaceOrientation {
    return NO;
    }



    - (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    }


    #pragma mark Touch Methods


    - (void) touchesBegan

    Please Register or Log in to view images

    NSSet*)touches withEvent

    Please Register or Log in to view images

    UIEvent*)event {
    [super touchesBegan:touches withEvent:event];

    NSArray *allTouches = [touches allObjects];
    UITouch *touch = [touches anyObject];

    int count = [allTouches count];

    if (count == 1) {
    if ([touch tapCount] < 2) {
    if (CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:0] locationInView:self.view])) {
    [UIView beginAnimations

    Please Register or Log in to view images

    "TouchDownAnimation" context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector

    Please Register or Log in to view images

    selector(finishedTouchDownAnimation:finished:context

    Please Register or Log in to view images

    ];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    [UIView setAnimationDuration:0.25];
    CGAffineTransform transform = CGAffineTransformMakeScale(1.1, 1.1);
    beachBall.transform = transform;
    beachBall.alpha = 0.85;
    [UIView commitAnimations];
    }
    }
    }

    if (count > 1) {
    touch1 = [[allTouches objectAtIndex:0] locationInView:self.view];
    touch2 = [[allTouches objectAtIndex:1] locationInView:self.view];
    }

    }

    - (void)touchesMoved

    Please Register or Log in to view images

    NSSet *)touches withEvent

    Please Register or Log in to view images

    UIEvent *)event {
    [super touchesMoved:touches withEvent:event];

    CGPoint currentTouch1;
    CGPoint currentTouch2;

    NSArray *allTouches = [touches allObjects];
    int count = [allTouches count];

    if (count == 1) {
    if (CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:0] locationInView:self.view])) {
    beachBall.center = [[allTouches objectAtIndex:0] locationInView:self.view];
    return;
    }
    }

    if (count > 1) {
    if ((CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:0] locationInView:self.view])) ||
    (CGRectContainsPoint([beachBall frame], [[allTouches objectAtIndex:1] locationInView:self.view]))) {

    currentTouch1 = [[allTouches objectAtIndex:0] locationInView:self.view];
    currentTouch2 = [[allTouches objectAtIndex:1] locationInView:self.view];

    CGFloat previousAngle = atan2(touch2.y - touch1.y, touch2.x - touch1.x) * 180 / M_PI;
    CGFloat currentAngle = atan2(currentTouch2.y - currentTouch1.y,currentTouch2.x - currentTouch1.x) * 180 / M_PI;

    CGFloat angleToRotate = currentAngle - previousAngle;

    CGAffineTransform transform = CGAffineTransformRotate(beachBall.transform, degreesToRadians(angleToRotate));

    beachBall.transform = transform;
    touch1 = currentTouch1;
    touch2 = currentTouch2;
    }

    }
    }

    - (void)touchesEnded

    Please Register or Log in to view images

    NSSet *)touches withEvent

    Please Register or Log in to view images

    UIEvent *)event {
    [super touchesEnded:touches withEvent:event];

    UITouch *touch = [touches anyObject];

    if ([touch tapCount] == 2) {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    [UIView setAnimationDuration:0.20];
    beachBall.center = self.view.center;
    beachBall.transform = CGAffineTransformIdentity;
    [UIView commitAnimations];

    return;
    }
    }



    #pragma mark Animation Selectors


    - (void)finishedTouchDownAnimation

    Please Register or Log in to view images

    NSString*)animationID finished

    Please Register or Log in to view images

    BOOL)finished context

    Please Register or Log in to view images

    void *)context {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    [UIView setAnimationDuration:0.25];
    CGAffineTransform transform = CGAffineTransformMakeScale(1.0, 1.0);
    beachBall.transform = transform;
    beachBall.alpha = 1.0;
    [UIView commitAnimations];
    }

    - (void)dealloc {
    [beachball release];
    [super dealloc];
    }

    @end
    [/OBJC]


    The first thing you want to do is set up a few things. Where you do this is entirely up to you, and what suits your application best. I prefer to viewDidLoad method. Go ahead and enable multi touch gestures and add the image view containing the beach ball image to your view. And to be sure, we don't want the application auto rotating on us.

    Really, the only methods we want to override are the touch methods. In the touchesBegan method, go ahead and count how many times the user has touched and tapped the screen.

    If the touch count is only one, and the user has tapped the screen only once, check to see if the user touched the beach ball. If so, we'll start an animation block. Although not required in most animation blocks, we want to set our view controller as the animation's delegate and set an animationDidStop selector. Doing so will allow us to call our own method when thee animation stops.

    Regardless of whether or not the animation finishes, we'll still know when it stops. This is important. Setting a timer could cause the the follow up method to perform before the animation is completed, ie if the animation lags for one of any number of reasons. Waiting for completion of the animation is also a fail, because an animation might not finish animating completely, for any number of reasons. In any case, we will know.

    In this same animation block, we will expand the image to 110% of its original size and set it's alpha to 85%. Scrolling to the bottom of this file, you will find the animationDidStop selector. Notice that we don't really need to know when this animation is stopped. Feel free to leave out the setAnimationDelegate and setAnimationDidStopSelector messages. Make sure to reset the original values of our beach ball.

    If the user touches a second finger (or a third, fourth, or fifteenth finger) to the screen, we want to register the points of two fingers. Use the first and second finger to touch the screen for simplicity's sake.

    In the touchesMoved, things get interested. First, we want to know how many fingers the user is moving across the screen. If only one finger is being used, let's go ahead and move our beach ball to where the finger is.

    If more than one finger are being used, and at least one finger is on the beach ball itself, let's start the rotation method. Get the angle of where the fingers were last by using a built in arctangent function. atan2() is what we want. Is different than atan() in that it takes two inputs. The arctangent will return the angle (in radians) between a coordinate and the x axis. [And you thought you'd never use trigonometry outside of school!] Using the same atan2 function, get the angle of the where the fingers currently are.

    The next optional step I take is to convert both angles into degrees from their radian value. That can be calculated like so: degrees = radians * (180 / pi). Remember that a radian is the measure of an arc that extends along a circle's circumference for the distance of the circle's radius. [Correct me if I'm wrong, I only took a couple of days of trig before I said, "Screw This!" and left]

    Finally, and this step is often missed, get the difference between the angles. If the fingers started out at 89 degrees, and we moved to 94 degrees, we only moved our fingers five degrees. Accordingly, we only want to move the beach ball five degrees. Create a rotational transform out of the difference, and apply it to the beach ball. Make sure to reset the original touch points to the current touch points, else your touchesMoved will never show any more change than the initial rotation.

    In your touchesEnded method, all we are really doing is looking for the end of a double tap. If detected, reset the center of the beach ball to the center of the screen, and reset its transform to its identity transform.

    And you are now done. If you actually were to release an application based off of this code, then please add in a flag for the touchesMoved that lifts upon touchesEnded that controls whether to move the beach ball if one finger is stopped/lifted. In a real life application, you wouldn't want that. Until both fingers are lifted, keep on rotating the ball. Setting a simple boolean will fix that.



    Source Code: http://www.touchrepo.com/SampleCode/TEST_SPINVIEW_APP.zip

    EDIT: Added Screenshot. Boldened link to Sample Code.
  2. cocotutch

    cocotutch Community Development Team Staff Member

    Joined:
    Oct 6, 2008
    Messages:
    1,285
    Likes Received:
    117
    Device:
    5G iPod touch
    I am now officially speechless.

    Thankyou so so so so so (times 2.5 million) much SkylarEC ! ! ! ! ! !

    I am going to post a screenshot of my application I implemented the "Spin" code into my Original Thread.

    cocotutch
  3. JoshuaCaputo

    JoshuaCaputo New Member

    Joined:
    Aug 2, 2008
    Messages:
    605
    Likes Received:
    0
    Device:
    iPod touch
    Wow, you are whipping these out quickly, you need a site for these

    Please Register or Log in to view images



    They are the best around. No Doubt
  4. Teslanaut

    Teslanaut Well-Known Member

    Joined:
    Sep 16, 2007
    Messages:
    15,588
    Likes Received:
    177
    Device:
    4G iPod touch
    Why not post these on a site like Pastie? Because its messing up the formatting of the page.
  5. JoshuaCaputo

    JoshuaCaputo New Member

    Joined:
    Aug 2, 2008
    Messages:
    605
    Likes Received:
    0
    Device:
    iPod touch
    Nah, he just needs his own site with a nice wide design. I'd be glad to offer my web services up if you need designer or host.
  6. SkylarEC

    SkylarEC Super Moderator Emeritus Staff Member

    Joined:
    Sep 19, 2007
    Messages:
    6,642
    Likes Received:
    129
    Buddy, don't forget that I ran a Community Source back in the day. I have all the hosting in the world.
  7. JoshuaCaputo

    JoshuaCaputo New Member

    Joined:
    Aug 2, 2008
    Messages:
    605
    Likes Received:
    0
    Device:
    iPod touch
    Alright, wasn't attempting to offend you .
  8. badawe

    badawe New Member

    Joined:
    Sep 19, 2008
    Messages:
    1
    Likes Received:
    0
    Dudes..

    I have some question!

    If i want animate one button, in the viewDidLoad, from scale 0.0, to scale 1.0, how i can do this in the same funcion? Animate he growing i can use like in your tutorial, but how i can set the initial state of my object?

    Someting like that:

    //setting the initial scale of my object to 0.0
    [heartBtn setTransform: CGAffineTransformMakeScale(0.0, 0.0)]

    //Animating his growing!
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
    [UIView setAnimationDuration:0.25];
    CGAffineTransform initTransform = CGAffineTransformMakeScale(1.0, 1.0);
    [heartBtn setTransform:initTransform];
    [UIView commitAnimations];
  9. Taitac

    Taitac New Member

    Joined:
    Dec 29, 2008
    Messages:
    76
    Likes Received:
    0
    Device:
    2G iPod touch
    Skylar thanks for this tut. I have a question.
    Will this method work for a chipmunk or cocos2d object as well ?
    I mean can i combine these 2 methods together?
    Create a chipmunk object in its own little space and use this method to move it around?

    Thanks
  10. SkylarEC

    SkylarEC Super Moderator Emeritus Staff Member

    Joined:
    Sep 19, 2007
    Messages:
    6,642
    Likes Received:
    129
    I have no experience using cocos2d or chipmunk. I can not answer your question. My best advice is to just try it and find out.

Share This Page