iOS >> Dragged View is Jumping Back to Original Position >> Auto Layout Combined with UIPanGestureRecognizer Issue
Asked Answered
S

2

14

I have several UIViews that the user should be able to drag & drop. I use UIPanGestureRecognizer to manage the d&d action (see code below). The d&d action is working fine, but when I start dragging another UIView, the one that I just dropped is jumping back to its original position.

- (void)handlePan:(UIPanGestureRecognizer *)recognizer {

    CGPoint translation = [recognizer translationInView:self.view];

    switch (recognizer.state) {
        case UIGestureRecognizerStateBegan:
            for (UIView* checkView in dragAndDropImageViewsArray) {
                if (checkView == recognizer.view) {
                    [checkView.superview bringSubviewToFront:checkView]; //this is done to make sure the currently dragged object is the top one
                }
            }
            break;

        case UIGestureRecognizerStateChanged:
            recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x, recognizer.view.center.y + translation.y);
            break;

        case UIGestureRecognizerStateEnded:
            NSLog(@"UIGestureRecognizerStateEnded");
            break;

        default:
            break;
    }

    [recognizer setTranslation:CGPointMake(0, 0) inView:self.view];

}

I tried several things that "sloved" the problem but caused other problems:

  • Cancel Auto-Layout: That totally solves it but now I have to start calculate and manage object locations and sizes for different devices and orientations.
  • Use layer.zPosition instead of bringSubviewToFront: that makes objects appear above other objects while apparently avoiding the "jump back", but does not allow me to change the hierarchy between the different draggable objects.

I started deepening my understanding about Auto Layout, but it gets very complicated when it seems to me that what I'm looking for should be very simple; I saw many methods that deal with performing a "re-auto-layouting", but I'm not clear about how to work with them.

Is there a simple solution here? like calling a method that overrides the IB Auto-Layout constrains and redefine them according to the UIView new position?

Any assistance would be very musch appreciated.

Suspension answered 21/4, 2013 at 10:1 Comment(0)
M
11

I think that if you are using autolayout and you want to change the position of a view, you need to be updating the constraints on the view instead of manually setting the center of a view. From the description it sounds like you are setting the center, but when the app next lays out the views (eg you drag another) it re-sets the center to whatever autolayout says it should be.

I'm not sure what the rest of your app looks like, but you may be able to find nearby views and add constraints to it, or you might prefer to just update the constraints of the dragged view and set the leading space and top space to the superview manually. If you've got a collection handy you could also clear all the constraints and then re-add them as you'd like to keep padding in between the views.

To set the constraints in code you can clear the constraints for the moved view and re-add them, or if you have a view in IB you can drag the constraints into code to create an outlet. Look at the removeConstraints and addConstraints methods to do it in code.

Lastly, if it works how you'd like without autolayout, you could try setting translatesAutoresizingMaskIntoConstraints = YES on the moved views, but be aware that may cause conflicts elsewhere.

Martsen answered 21/4, 2013 at 19:19 Comment(2)
Thanks James - Can you be a bit more specific about how to use the removeConstraints and addConstraints methods? How do I capture the "new constraints" array once the view was dropped at its new position? If there's a good tutorial about it that you can direct me to - I would really appreciate it.Suspension
Try the Managing Constraints section of the UIView documentation to add/remove. If you just want it to stay where it landed you could try setting the top padding and leading padding to the view. With visual constraints, setting a view to 400 from the top and 200 from the leading edge would look like [NSLayoutConstraint constraintsWithVisualFormat:@"|-(=200)-[movedView]" options:nil metrics:nil views:@{@"movedView": recognizer.view}] [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(=400)-[movedView]" options:nil metrics:nil views:@{@"movedView": recognizer.view}]Martsen
M
7

This is all possible using Interface Builder. Here is the code I used to do this:

@property (weak,nonatomic) IBOutlet NSLayoutConstraint *buttonXConstraint;
@property (weak,nonatomic) IBOutlet NSLayoutConstraint *buttonYConstraint;

Then hook up those IBOutlets to the Horizontal Constraint(X Position) and Vertical Constraint(Y Position) in the Interface Builder. Make sure that the Constraints are hooked up to the base view and not it's closest view.

Hook up a pan gesture and then use this following code to drag your object around:

- (IBAction)panPlayButton:(UIPanGestureRecognizer *)sender
{
    if(sender.state == UIGestureRecognizerStateBegan){

    } else if(sender.state == UIGestureRecognizerStateChanged){
        CGPoint translation = [sender translationInView:self.view];

        //Update the constraint's constant
        self.buttonXConstraint.constant += translation.x;
        self.buttonYConstraint.constant += translation.y;

        // Assign the frame's position only for checking it's fully on the screen
        CGRect recognizerFrame = sender.view.frame;
        recognizerFrame.origin.x = self.buttonXConstraint.constant;
        recognizerFrame.origin.y = self.buttonYConstraint.constant;

        // Check if UIImageView is completely inside its superView
        if(!CGRectContainsRect(self.view.bounds, recognizerFrame)) {
            if (self.buttonYConstraint.constant < CGRectGetMinY(self.view.bounds)) {
                self.buttonYConstraint.constant = 0;
            } else if (self.buttonYConstraint.constant + CGRectGetHeight(recognizerFrame) > CGRectGetHeight(self.view.bounds)) {
                self.buttonYConstraint.constant = CGRectGetHeight(self.view.bounds) - CGRectGetHeight(recognizerFrame);
            }

            if (self.buttonXConstraint.constant < CGRectGetMinX(self.view.bounds)) {
                self.buttonXConstraint.constant = 0;
            } else if (self.buttonXConstraint.constant + CGRectGetWidth(recognizerFrame) > CGRectGetWidth(self.view.bounds)) {
                self.buttonXConstraint.constant = CGRectGetWidth(self.view.bounds) - CGRectGetWidth(recognizerFrame);
            }
        }

        //Layout the View
        [self.view layoutIfNeeded];
    } else if(sender.state == UIGestureRecognizerStateEnded){

    }

    [sender setTranslation:CGPointMake(0, 0) inView:self.view];
}

I have added code in there to check the frame and make sure that it's not going outside the view. If you want to allow the object to be partially off the screen, feel free to take it out.

This took a little bit of time for me to realize that it was the use of AutoLayout that was resetting the view but constraints will do what you need here!

Mashe answered 19/3, 2014 at 3:33 Comment(1)
This worked for me. However, checking if the frame position is fully in screen is restricting the movement much less than the super view bounds.Fatherless

© 2022 - 2024 — McMap. All rights reserved.