When moving a UILabel vertically with a UIPanGestureRecognizer, how do I stop them from going too far in either direction?

StackOverflow https://stackoverflow.com/questions/19660755

Question

I'm trying to make the user be able to move the UILabel up and down across the view by attaching a UIPanGestureRecognizer to the UILabel and subsequently altering the constant of a constraint from the UILabel to the top of its view. So basically if the gesture recognizer detects them moving down 12pts, move the constant of the constraint 12pts to move the UILabel.

However, I want them to be stopped from moving further when they hit a certain vertical point (both too high or too low). I could just check the translation of the pan gesture, but my UILabel can be any number of lines, so if it's five lines instead of one, obviously it can't be panned down quite as far, so I can't rely on the translation of the pan gesture, I have to take into account the size of the label.

So I started monitoring its frame, and it works well, but in my implementation there's an annoying result where if they pan completely to the bottom limit, they have to pan really far back up before the UILabel "catches up" and comes with it (though no such problem exists when they hit the top boundary). Basically, they pan down to the bottom limit, and when they pan back up (this is all in the same gesture) it "sticks" momentarily until they pan far enough up, then it jumps up with their finger.

Here's the code I'm using to accomplish this:

- (void)textLabelPanned:(UIPanGestureRecognizer *)panGestureRecognizer {
    if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
        _textDistanceFromTopBeforeMove = self.textToReadLabelPositionFromTopConstraint.constant;
    }
    else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
        NSNumber *textDistanceFromTop = @(self.textToReadLabelPositionFromTopConstraint.constant);
        [[NSUserDefaults standardUserDefaults] setObject:textDistanceFromTop forKey:@"TextDistanceFromTop"];
    }
    else {
        if (CGRectGetMinY(self.textToReadLabel.frame) >= [UIScreen mainScreen].bounds.origin.y + CLOSEST_TEXT_DISTANCE_TO_TOP && CGRectGetMaxY(self.textToReadLabel.frame) <= [UIScreen mainScreen].bounds.size.height - CLOSEST_TEXT_DISTANCE_TO_BOTTOM) {
                self.textToReadLabelPositionFromTopConstraint.constant = _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y;
        }
        else if ([panGestureRecognizer translationInView:self.mainView].y > 0) {
            if (CGRectGetMaxY(self.textToReadLabel.frame) + _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y < [UIScreen mainScreen].bounds.size.height - CLOSEST_TEXT_DISTANCE_TO_BOTTOM) {
                self.textToReadLabelPositionFromTopConstraint.constant = _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y;
            }
        }
        else if ([panGestureRecognizer translationInView:self.mainView].y < 0) {
            if (CGRectGetMinY(self.textToReadLabel.frame) + _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y > [UIScreen mainScreen].bounds.origin.y + CLOSEST_TEXT_DISTANCE_TO_TOP) {
                self.textToReadLabelPositionFromTopConstraint.constant = _textDistanceFromTopBeforeMove + [panGestureRecognizer translationInView:self.mainView].y;
            }
        }


        // If one of the options views are present and the user pans really low, hide the options as to allow the user to see where they're panning
        if (_inSpeedChangingMode) {
            if (CGRectGetMaxY(self.textToReadLabel.frame) > CGRectGetMinY(self.progressBar.frame) - 10) {
                [self showWordOptions:nil];
            }
        }
        else if (_inTextChangingMode) {
            if (CGRectGetMaxY(self.textToReadLabel.frame) > CGRectGetMinY(self.progressBar.frame) - 10) {
                [self showTextOptions:nil];
            }
        }
    }
}

What exactly am I doing wrong that would be causing it to "stick"? And is there perhaps a better way to be doing this?

Was it helpful?

Solution

You can accomplish this entirely with constraints, defined either in Interface Builder, or in code. The trick is to define constraints that prevent the label from moving out of bounds that have higher priority than the constraints that set the desired position.

In my test project I set up a view hierarchy entirely in a storyboard having a 1) view controller view 2) "container view" which defines the bounds 3) multi-line UILabel. There are 6 constraints acting on the label from its container:

  • 4 'space to' constraints (leading, trailing, top, bottom) prevent the label from ever being positioned outside the bounds of its parent container. The priority on these is set to the default '1000' value in Interface Builder. The relation for these constraints is '>=', and the constant value is '0'.

  • 2 'space to' constraints (leading, top) drive the label's actual position. The priority on these is set lower; I chose '500'. These constraints have outlets in the view controller so they can be adjusted in code. The relation for these constraints is '=', and the initial value is whatever you want to position the label.

The label itself has a width constraint to force it to display with multiple lines.

Here's what this looks like in IB:

The selected constraint has a lower priority and is used to drive the x position of the label. This constraint is tied to an ivar in the view controller so it can be adjusted at runtime. showing a positioning constraint selected

The selected constraint has a higher priority and is used to corral the label within its parent view. showing a corralling constraint selected

And here is the code in the view controller:

@interface TSViewController ()
@end

@implementation TSViewController
{
    IBOutlet NSLayoutConstraint* _xLayoutConstraint;

    IBOutlet NSLayoutConstraint* _yLayoutConstraint;
}

- (IBAction) pan: (UIGestureRecognizer*) pgr
{
    CGPoint p = [pgr locationInView: self.view];

    p.x -= pgr.view.frame.size.width / 2.0;
    p.y -= pgr.view.frame.size.height / 2.0;

    _xLayoutConstraint.constant = p.x;
    _yLayoutConstraint.constant = p.y;
}

@end

The UIPanGestureRecognizer is associated with the UILabel and has its callback set to the pan: method in the view controller.

OTHER TIPS

If your app has minimum SDK iOS7, you can use UIKit Dynamics instead of those UIGestureRecognizers. Your problem could be easily solved with a UICollisionBehavoir combined with an UIAttachmentBehavior

You might want to look into it. Here's the apple sample project on UIKit Dynamics: https://developer.apple.com/library/IOS/samplecode/DynamicsCatalog/Introduction/Intro.html

Play around with it and you'll be amazed what you can do with so little code.

WWDC 2013 sessions: - Getting Started with UIKit Dynamics - Advanced Techniques with UIKit Dynamics

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top