Dragging views using UIAttachmentBehavior

6.9k views Asked by At

I would like to drag one UIView, and have it move the position of other views attached to it as if they were all attached via string. The starting state would be a bunch of UIViews clumped together. If you pull one out, the item it's attached to will begin to move once the minimum distance is met between it and the view being dragged. I was thinking that the best way to accomplish this would be using UIDynamicItemBehavior considering I need it to snap back into place and have weight applied to it. I'm not quite sure how to accomplish this without doing ridiculous code otherwise. The code I do have can be seen below. Unfotunately, I'm having issues with the square item being dragged snapping back to square2. Would anyone have any advice, I'll be happy to clarify if that is needed.

- (void)viewDidLoad
{
    [super viewDidLoad];

    _containmentView = [[UIView alloc] initWithFrame:CGRectMake(0.0, self.view.frame.size.height/2, self.view.frame.size.width, 200)];
    [self.view addSubview:_containmentView];
    [_containmentView setBackgroundColor:[UIColor greenColor]];

    _square = [[UIView alloc]initWithFrame:CGRectMake(200, 0, 100, 100)];
    _square.backgroundColor = [UIColor yellowColor];
    [_containmentView addSubview:_square];

    _square2 = [[UIView alloc]initWithFrame:CGRectMake(100, 0, 100, 100)];
    _square2.backgroundColor = [UIColor redColor];
    [_containmentView addSubview:_square2];


    _animator = [[UIDynamicAnimator alloc]initWithReferenceView:_containmentView];
    _gravity = [[UIGravityBehavior alloc]initWithItems:@[_square, _square2]];
    _gravity.gravityDirection = CGVectorMake(-1.0, 0.0);
    [_animator addBehavior:_gravity];

    _collision = [[UICollisionBehavior alloc]initWithItems:@[_square]];
    _collision.collisionDelegate = self;
    _collision.translatesReferenceBoundsIntoBoundary = YES; //causes the boundary to use the bounds of the reference view supplied to the UIDynamicAnimator




    [_animator addBehavior:_collision];


    UIDynamicItemBehavior *itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[_square]];
    itemBehaviour.elasticity = 0.6;
    [_animator addBehavior:itemBehaviour];

    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [_square addGestureRecognizer:gesture];

    UIAttachmentBehavior *attach = [[UIAttachmentBehavior alloc] initWithItem:_square2 attachedToItem:_square];
    [_animator addBehavior:attach];

}

-(void)handlePan:(UIPanGestureRecognizer *)gesture
{
    CGPoint touchPoint = [gesture locationInView:self.view];
    UIView* draggedView = gesture.view;

    if (gesture.state == UIGestureRecognizerStateBegan) {
        // 1. was the pan initiated from the upper part of the recipe?
        UIView* draggedView = gesture.view;
        CGPoint dragStartLocation = [gesture locationInView:draggedView];
            _draggingView = YES;
            _previousTouchPoint = touchPoint;
//            [_animator updateItemUsingCurrentState:draggedView];

    } else if (gesture.state == UIGestureRecognizerStateChanged && _draggingView) {
        // 2. handle dragging
        float xOffset = _previousTouchPoint.x - touchPoint.x;
        gesture.view.center = CGPointMake(draggedView.center.x - xOffset,
                                          draggedView.center.y);
        _previousTouchPoint = touchPoint;
//        [_animator updateItemUsingCurrentState:draggedView];

    } else if (gesture.state == UIGestureRecognizerStateEnded && _draggingView) {
        // 3. the gesture has ended
//        [self tryDockView:draggedView];
//        [self addVelocityToView:draggedView fromGesture:gesture];
        [_animator updateItemUsingCurrentState:draggedView];
        _draggingView = NO;
    }

}
1

There are 1 answers

3
Rufel On

You need to use an attachment to drag your shape, so the animator can calculate every items positions using its physic engine.

Here is you viewDidLoad updated:

- (void)viewDidLoad
{
    [super viewDidLoad];

    _containmentView = [[UIView alloc] initWithFrame:CGRectMake(0.0, self.view.frame.size.height/2, self.view.frame.size.width, 200)];
    [self.view addSubview:_containmentView];
    [_containmentView setBackgroundColor:[UIColor greenColor]];

    _square = [[UIView alloc]initWithFrame:CGRectMake(200, 0, 100, 100)];
    _square.backgroundColor = [UIColor yellowColor];
    [_containmentView addSubview:_square];

    _square2 = [[UIView alloc]initWithFrame:CGRectMake(100, 0, 100, 100)];
    _square2.backgroundColor = [UIColor redColor];
    [_containmentView addSubview:_square2];

    _animator = [[UIDynamicAnimator alloc]initWithReferenceView:_containmentView];
    _gravity = [[UIGravityBehavior alloc]initWithItems:@[_square, _square2]];
    _gravity.gravityDirection = CGVectorMake(-1.0, 0.0);
    [_animator addBehavior:_gravity];

    _collision = [[UICollisionBehavior alloc]initWithItems:@[_square, _square2]];
    _collision.translatesReferenceBoundsIntoBoundary = YES;

    [_animator addBehavior:_collision];


    UIDynamicItemBehavior *itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[_square, _square2]];
    itemBehaviour.elasticity = 0.6;
    [_animator addBehavior:itemBehaviour];

    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [_square addGestureRecognizer:gesture];

    UIPanGestureRecognizer *gesture2 = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [_square2 addGestureRecognizer:gesture2];

    UIAttachmentBehavior *attach = [[UIAttachmentBehavior alloc] initWithItem:_square2 attachedToItem:_square];
    attach.damping = 1.6;
    attach.frequency = 10;
    [_animator addBehavior:attach];

}

I added gravity, pan & collisions to both shapes.

Here what should look your handlePan method:

-(void)handlePan:(UIPanGestureRecognizer *)gesture
{
    CGPoint touchPoint = [gesture locationInView:_containmentView];
    UIView* draggedView = gesture.view;

    if (gesture.state == UIGestureRecognizerStateBegan) {
        // 1. was the pan initiated from the upper part of the recipe?
        _draggingView = YES;
        _previousTouchPoint = touchPoint;
        // Add a new attachment on the selected view;
        self.attachment = [[UIAttachmentBehavior alloc] initWithItem:draggedView attachedToAnchor:touchPoint];
        [self.animator addBehavior:self.attachment];
        // Could temporarily remove gravity here

    } else if (gesture.state == UIGestureRecognizerStateChanged && _draggingView) {
        // 2. handle dragging
        [self.attachment setAnchorPoint:touchPoint];

    } else if (gesture.state == UIGestureRecognizerStateEnded && _draggingView) {
        // 3. the gesture has ended
        _draggingView = NO;
        [self.animator removeBehavior:self.attachment];
        // If gravity was removed, add it back here
    }
}

You may want to remove gravity temporarily while dragging. Just take a look at my comments to see where you could do it (and don't forget to put it back when the dragging's end).

As you can see, you really need less coding to handle gesture that way :).

You can play with both attachment's damping, frequency and length to make shapes react differently to the drag gesture. You can also add a density to your shapes using the UIDynamicItemBehavior's density property.