Oktay
Oktay

Reputation: 531

Scroll effect with swipe gesture in iOS

I have a view which has two labels. When I swipe left I fill next content to label text. Similarly swiping right loads previous content. I want to give an effect to labels like they are scrolling from left or right. I used a scrollview before but it had a memory problem. So I'm using one view, and swipe gesture loads next or previous content. I want to add scrollview's sliding effect to labels. How can I do that?

Upvotes: 6

Views: 12216

Answers (2)

Rob
Rob

Reputation: 437872

If you prefer animation that behaves more like a scroll view (i.e., with continuous feedback on the gesture), it might look something like the following:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panHandler:)];
    [self.view addGestureRecognizer:pan];

    self.label1.text = @"Mo";
}

- (void)panHandler:(UIPanGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateBegan)
    {
        _panLabel = [[UILabel alloc] init];

        // in my example, I'm just going to toggle the value between Mo and Curly
        // you'll presumably set the label contents based upon the direction of the
        // pan (if positive, swiping to the right, grab the "previous" label, if negative
        // pan, grab the "next" label)

        if ([self.label1.text isEqualToString:@"Curly"])
            _newText = @"Mo";
        else
            _newText = @"Curly";

        // set the text

        _panLabel.text = _newText;

        // set the frame to just be off screen

        _panLabel.frame = CGRectMake(self.label1.frame.origin.x + self.containerView.frame.size.width,
                                     self.label1.frame.origin.y, 
                                     self.label1.frame.size.width, 
                                     self.label1.frame.size.height);

        [self.containerView addSubview:_panLabel];

        _originalCenter = self.label1.center; // save where the original label originally was
    }
    else if (sender.state == UIGestureRecognizerStateChanged)
    {
        CGPoint translate = [sender translationInView:self.containerView];

        if (translate.x > 0)
        {
            _panLabel.center = CGPointMake(_originalCenter.x - self.containerView.frame.size.width + translate.x, _originalCenter.y);
            self.label1.center = CGPointMake(_originalCenter.x + translate.x, _originalCenter.y);
        }
        else 
        {
            _panLabel.center = CGPointMake(_originalCenter.x + self.containerView.frame.size.width + translate.x, _originalCenter.y);
            self.label1.center = CGPointMake(_originalCenter.x + translate.x, _originalCenter.y);
        }
    }
    else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateFailed || sender.state == UIGestureRecognizerStateCancelled)
    {
        CGPoint translate = [sender translationInView:self.containerView];
        CGPoint finalNewFieldLocation;
        CGPoint finalOriginalFieldLocation;
        BOOL panSucceeded;

        if (sender.state == UIGestureRecognizerStateFailed ||
            sender.state == UIGestureRecognizerStateCancelled)
        {
            panSucceeded = NO;
        }
        else 
        {
            // by factoring in the velocity, we can capture a flick more accurately
            //
            // (by the way, I don't like iOS's velocity, because if you stop moving, it records the velocity
            // prior to stopping the move rather than noting that you actually stopped, so I usually calculate my own, 
            // but I'll leave this as is for purposes of this example)

            CGPoint velocity  = [sender velocityInView:self.containerView];

            if (translate.x < 0)
                panSucceeded = ((translate.x + velocity.x * 0.5) < -(self.containerView.frame.size.width / 2));
            else
                panSucceeded = ((translate.x + velocity.x * 0.5) > (self.containerView.frame.size.width / 2));
        }

        if (panSucceeded)
        {
            // if we succeeded, finish moving the stuff

            finalNewFieldLocation = _originalCenter;
            if (translate.x < 0)
                finalOriginalFieldLocation = CGPointMake(_originalCenter.x - self.containerView.frame.size.width, _originalCenter.y);
            else
                finalOriginalFieldLocation = CGPointMake(_originalCenter.x + self.containerView.frame.size.width, _originalCenter.y);
        }
        else
        {
            // if we didn't, then just return everything to where it was

            finalOriginalFieldLocation = _originalCenter;

            if (translate.x < 0)
                finalNewFieldLocation = CGPointMake(_originalCenter.x + self.containerView.frame.size.width, _originalCenter.y);
            else
                finalNewFieldLocation = CGPointMake(_originalCenter.x - self.containerView.frame.size.width, _originalCenter.y);
        }

        // animate the moving of stuff to their final locations, and on completion, clean everything up

        [UIView animateWithDuration:0.3
                              delay:0.0 
                            options:UIViewAnimationOptionCurveEaseOut
                         animations:^{
                             _panLabel.center = finalNewFieldLocation;
                             self.label1.center = finalOriginalFieldLocation;
                         }
                         completion:^(BOOL finished) {
                             if (panSucceeded)
                                 self.label1.text = _newText;
                             self.label1.center = _originalCenter;
                             [_panLabel removeFromSuperview];
                             _panLabel = nil; // in non-ARC, release instead
                         }
         ];
    }
}   

Note, I have put both the original label, as well as the new label being panned on, in a container UIView (called containerView, surprisingly enough), so that I can clip the animation to that container.

Upvotes: 6

Rob
Rob

Reputation: 437872

I'm not quite sure precisely what effect you're looking for, but you could do something like this, which creates a new, temporary label, puts it off screen, animates the moving it over the label you have on screen, and then when done, resets the old one and deletes the temporary label. This is what a non-autolayout implementation might look like:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    UISwipeGestureRecognizer *left = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(leftSwipe:)];
    [left setDirection:UISwipeGestureRecognizerDirectionLeft];
    [self.view addGestureRecognizer:left];
    // if non-ARC, release it
    // [release left];

    self.label1.text = @"Mo";
}

- (void)leftSwipe:(UISwipeGestureRecognizer *)gesture
{
    NSString *newText;
    UILabel  *existingLabel = self.label1;

    // in my example, I'm just going to toggle the value between Mo and Curly

    if ([existingLabel.text isEqualToString:@"Curly"])
        newText = @"Mo";
    else
        newText = @"Curly";

    // create new label

    UILabel *tempLabel = [[UILabel alloc] initWithFrame:existingLabel.frame];
    [existingLabel.superview addSubview:tempLabel];
    tempLabel.text = newText;

    // move the new label off-frame to the right

    tempLabel.transform = CGAffineTransformMakeTranslation(tempLabel.superview.bounds.size.width, 0);

    // animate the sliding of them into place

    [UIView animateWithDuration:0.5
                     animations:^{
                         tempLabel.transform = CGAffineTransformIdentity;
                         existingLabel.transform = CGAffineTransformMakeTranslation(-existingLabel.superview.bounds.size.width, 0);
                     }
                     completion:^(BOOL finished) {
                         existingLabel.text = newText;
                         existingLabel.transform = CGAffineTransformIdentity;
                         [tempLabel removeFromSuperview];
                     }];

    // if non-ARC, release it
    // [release tempLabel];
}

This animation animates the label with respect to its superview. You may want to ensure that the superview is set to "clip subviews". This way, the animation will be constrained to the bounds of that superview, which yields a slightly more polished look.

Note, if using auto layout, the idea is the same (though the execution is more complicated). Basically configure your constraints so new view is off to the right, then, in animation block update/replace the constraints so the original label is off to the left and the new one is in the spot of the original label, and, finally, in the completion block reset the constraints of the original label and remove the temporary label.


By the way, this is all infinitely easier if you're comfortable with one of the built in transitions:

- (void)leftSwipe:(UISwipeGestureRecognizer *)gesture
{
    NSString *newText;
    UILabel  *existingLabel = self.label1;

    // in my example, I'm just going to toggle the value between Mo and Curly

    if ([existingLabel.text isEqualToString:@"Curly"])
        newText = @"Mo";
    else
        newText = @"Curly";

    [UIView transitionWithView:existingLabel  // or try `existingLabel.superview`
                      duration:0.5
                       options:UIViewAnimationOptionTransitionFlipFromRight
                    animations:^{
                        existingLabel.text = newText;
                    }
                    completion:nil];
}

Upvotes: 15

Related Questions