user1491987 user1491987 - 1 month ago 15
iOS Question

Recognize a path of user gesture

I'm working on an app with 9 views on screen, and I want the users to connect the views in a way they want, and record their sequence as password.
But I don't know which gesture recognizer I should use.

Should I use CMUnistrokeGestureRecognizer or combination of several swipe gesture or anything else?
Thanks.

Rob Rob
Answer

You could use a UIPanGestureRecognizer, something like:

CGFloat const kMargin = 10;

- (void)viewDidLoad
{
    [super viewDidLoad];

    // create a container view that all of our subviews for which we want to detect touches are:

    CGFloat containerWidth = fmin(self.view.bounds.size.width, self.view.bounds.size.height) - kMargin * 2.0;

    UIView *container = [[UIView alloc] initWithFrame:CGRectMake(kMargin, kMargin, containerWidth, containerWidth)];
    container.backgroundColor = [UIColor darkGrayColor];
    [self.view addSubview:container];

    // now create all of the subviews, specifying a tag for each; and

    CGFloat cellWidth = (containerWidth - (4.0 * kMargin)) / 3.0;

    for (NSInteger column = 0; column < 3; column++)
    {
        for (NSInteger row = 0; row < 3; row++)
        {
            UIView *cell = [[UIView alloc] initWithFrame:CGRectMake(kMargin + column * (cellWidth + kMargin),
                                                                    kMargin + row    * (cellWidth + kMargin),
                                                                    cellWidth, cellWidth)];
            cell.tag = row * 3 + column;
            cell.backgroundColor = [UIColor lightGrayColor];
            [container addSubview:cell];
        }
    }

    // finally, create the gesture recognizer

    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self
                                                                          action:@selector(handlePan:)];
    [container addGestureRecognizer:pan];
}

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    static NSMutableArray *gesturedSubviews;

    // if we're starting a gesture, initialize our list of subviews that we've gone over

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        gesturedSubviews = [NSMutableArray array];
    }

    // now figure out whether:
    //   (a) are we over a subview; and
    //   (b) is this a different subview than we last were over

    CGPoint location = [gesture locationInView:gesture.view];

    for (UIView *subview in gesture.view.subviews)
    {
        if (CGRectContainsPoint(subview.frame, location))
        {
            if (subview != [gesturedSubviews lastObject])
            {
                [gesturedSubviews addObject:subview];

                // an example of the sort of graphical flourish to give the
                // some visual cue that their going over the subview in question 
                // was recognized

                [UIView animateWithDuration:0.25
                                      delay:0.0
                                    options:UIViewAnimationOptionAutoreverse
                                 animations:^{
                                     subview.alpha = 0.5;
                                 }
                                 completion:^(BOOL finished){
                                     subview.alpha = 1.0;
                                 }];
            }
        }
    }

    // finally, when done, let's just log the subviews
    // you would do whatever you would want here

    if (gesture.state == UIGestureRecognizerStateEnded)
    {
        NSLog(@"We went over:");

        for (UIView *subview in gesturedSubviews)
        {
            NSLog(@"  %d", subview.tag);
        }

        // you might as well clean up your static variable when you're done

        gesturedSubviews = nil;
    }
}

Obviously, you would create your subviews any way you want, and keep track of them any way you want, but the idea is to have subviews with unique tag numbers, and the gesture recognizer would just see which order you go over them in a single gesture.

Even if I didn't capture precisely what you want, it at least shows you how you can use a pan gesture recognizer to track the movement of your finger from one subview to another.


Update:

If you wanted to draw a path on the screen as the user is signing in, you could create a CAShapeLayer with a UIBezierPath. I'll demonstrate that below, but as a caveat, I feel compelled to point out that this might not be a great security feature: Usually with password entry, you'll show the user enough so that they can confirm that they're doing what they want, but not enough so that someone can glance look over their shoulder and see what the whole password was. When entering a text password, usually iOS momentarily shows you the last key you hit, but quickly turns that into an asterisk so that, at no point, can you see the whole password. Hence my initial suggestion.

But if you really have your heart set on showing the user the path as they draw it, you could use something like the following. First, this requires Quartz 2D. Thus add the QuartzCore.framework to your project (see Linking to a Library or Framework). Second, import the QuartCore headers:

#import <QuartzCore/QuartzCore.h>

Third, replace the pan handler with something like:

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    static NSMutableArray *gesturedSubviews;
    static UIBezierPath *path = nil;
    static CAShapeLayer *shapeLayer = nil;

    // if we're starting a gesture, initialize our list of subviews that we've gone over

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        gesturedSubviews = [NSMutableArray array];
    }

    // now figure out whether:
    //   (a) are we over a subview; and
    //   (b) is this a different subview than we last were over

    CGPoint location = [gesture locationInView:gesture.view];

    for (UIView *subview in gesture.view.subviews)
    {
        if (!path)
        {
            // if the path hasn't be started, initialize it and the shape layer

            path = [UIBezierPath bezierPath];
            [path moveToPoint:location];
            shapeLayer = [[CAShapeLayer alloc] init];
            shapeLayer.strokeColor = [UIColor redColor].CGColor;
            shapeLayer.fillColor = [UIColor clearColor].CGColor;
            shapeLayer.lineWidth = 2.0;
            [gesture.view.layer addSublayer:shapeLayer];
        }
        else
        {
            // otherwise add this point to the layer's path

            [path addLineToPoint:location];
            shapeLayer.path = path.CGPath;
        }

        if (CGRectContainsPoint(subview.frame, location))
        {
            if (subview != [gesturedSubviews lastObject])
            {
                [gesturedSubviews addObject:subview];

                [UIView animateWithDuration:0.25
                                      delay:0.0
                                    options:UIViewAnimationOptionAutoreverse
                                 animations:^{
                                     subview.alpha = 0.5;
                                 }
                                 completion:^(BOOL finished){
                                     subview.alpha = 1.0;
                                 }];
            }
        }
    }

    // finally, when done, let's just log the subviews
    // you would do whatever you would want here

    if (gesture.state == UIGestureRecognizerStateEnded)
    {
        // assuming the tags are numbers between 0 and 9 (inclusive), we can build the password here

        NSMutableString *password = [NSMutableString string];

        for (UIView *subview in gesturedSubviews)
            [password appendFormat:@"%c", subview.tag + 48];

        NSLog(@"Password = %@", password);

        // clean up our array of gesturedSubviews

        gesturedSubviews = nil;

        // clean up the drawing of the path on the screen the user drew

        [shapeLayer removeFromSuperlayer];
        shapeLayer = nil;
        path = nil;
    }
}

That yields a path that the user draws as the gesture proceeds:

path of user's finger

Rather than showing the path the user draws with each and every movement of the user's finger, maybe you just draw the lines between the center of the subviews, such as:

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    static NSMutableArray *gesturedSubviews;
    static UIBezierPath *path = nil;
    static CAShapeLayer *shapeLayer = nil;

    // if we're starting a gesture, initialize our list of subviews that we've gone over

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        gesturedSubviews = [NSMutableArray array];
    }

    // now figure out whether:
    //   (a) are we over a subview; and
    //   (b) is this a different subview than we last were over

    CGPoint location = [gesture locationInView:gesture.view];

    for (UIView *subview in gesture.view.subviews)
    {
        if (CGRectContainsPoint(subview.frame, location))
        {
            if (subview != [gesturedSubviews lastObject])
            {
                [gesturedSubviews addObject:subview];

                if (!path)
                {
                    // if the path hasn't be started, initialize it and the shape layer

                    path = [UIBezierPath bezierPath];
                    [path moveToPoint:subview.center];
                    shapeLayer = [[CAShapeLayer alloc] init];
                    shapeLayer.strokeColor = [UIColor redColor].CGColor;
                    shapeLayer.fillColor = [UIColor clearColor].CGColor;
                    shapeLayer.lineWidth = 2.0;
                    [gesture.view.layer addSublayer:shapeLayer];
                }
                else
                {
                    // otherwise add this point to the layer's path

                    [path addLineToPoint:subview.center];
                    shapeLayer.path = path.CGPath;
                }

                [UIView animateWithDuration:0.25
                                      delay:0.0
                                    options:UIViewAnimationOptionAutoreverse
                                 animations:^{
                                     subview.alpha = 0.5;
                                 }
                                 completion:^(BOOL finished){
                                     subview.alpha = 1.0;
                                 }];
            }
        }
    }

    // finally, when done, let's just log the subviews
    // you would do whatever you would want here

    if (gesture.state == UIGestureRecognizerStateEnded)
    {
        // assuming the tags are numbers between 0 and 9 (inclusive), we can build the password here

        NSMutableString *password = [NSMutableString string];

        for (UIView *subview in gesturedSubviews)
            [password appendFormat:@"%c", subview.tag + 48];

        NSLog(@"Password = %@", password);

        // clean up our array of gesturedSubviews

        gesturedSubviews = nil;

        // clean up the drawing of the path on the screen the user drew

        [shapeLayer removeFromSuperlayer];
        shapeLayer = nil;
        path = nil;
    }
}

That yields something like:

user path with lines

You have all sorts of options, but hopefully you now have the building blocks so you can design your own solution.