Drag Uibutton Without It Shifting to Center [Swift 3]

Drag UIButton without it shifting to center [Swift 3]

This can be done at least in two different ways, one using GestureRecognizer your question way and other way is subclassing the UIView and implementing the touchesBegan, touchesMoved , touchesEnded, touchesCancelled in general will work for any UIView subclass can be UIButton or UILabel or UIImageView etc...

In your way, using GestureRecognizer I make a few changes you still require a var to keep the origin CGPoint of the touch in your UIButton so we get the touch position relative to the UIButton and when the drag continue adjust the UIButton origin according to the origin touch position and the positions of the movement

Method 1 GestureRecognizer

import UIKit

class ViewController: UIViewController {

@IBOutlet weak var button: UIButton!
var buttonOrigin : CGPoint = CGPoint(x: 0, y: 0)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let gesture = UIPanGestureRecognizer(target: self, action: #selector(buttonDrag(pan:)))
self.button.addGestureRecognizer(gesture)
}

func buttonDrag(pan: UIPanGestureRecognizer) {
print("Being Dragged")
if pan.state == .began {
print("panIF")
buttonOrigin = pan.location(in: button)
}else {
print("panELSE")
let location = pan.location(in: view) // get pan location
button.frame.origin = CGPoint(x: location.x - buttonOrigin.x, y: location.y - buttonOrigin.y)
}
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

}

Method 2 UIView subclass in this case UIButton subclass

Use this UIButton subclass

import UIKit

class DraggableButton: UIButton {

var localTouchPosition : CGPoint?

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch = touches.first
self.localTouchPosition = touch?.preciseLocation(in: self)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let touch = touches.first
guard let location = touch?.location(in: self.superview), let localTouchPosition = self.localTouchPosition else{
return
}

self.frame.origin = CGPoint(x: location.x - localTouchPosition.x, y: location.y - localTouchPosition.y)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
self.localTouchPosition = nil
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
self.localTouchPosition = nil
}

}

Result

Sample Image

Sample Image

Hope this helps you

UIButton Frame changed After Move from one Position To Another

I believe the issue is that the rotation is about the center of the view, so when you apply the rotation the frame size increases which throws off the distance relative to the origin of the frame.

I was able to fix this by moving the view relative to its center:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch = touches.first
guard let location = touch?.location(in: self.superview) else { return }

// Store localTouchPosition relative to center
self.localTouchPosition = CGPoint(x: location.x - self.center.x, y: location.y - self.center.y)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let touch = touches.first
guard let location = touch?.location(in: self.superview), let localTouchPosition = self.localTouchPosition else{
return
}

self.center = CGPoint(x: location.x - localTouchPosition.x, y: location.y - localTouchPosition.y)
print(self.frame)
}

Touch and drag a UIButton around, but don't trigger it when releasing the finger

You could use a UIPanGestureRecognizer and tell it to cancel touches in view...

- (void)viewDidLoad
{
[super viewDidLoad];

UIPanGestureRecognizer *panRecognizer;
panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:@selector(wasDragged:)];
// cancel touches so that touchUpInside touches are ignored
panRecognizer.cancelsTouchesInView = YES;
[[self draggableButton] addGestureRecognizer:panRecognizer];

}

- (void)wasDragged:(UIPanGestureRecognizer *)recognizer {
UIButton *button = (UIButton *)recognizer.view;
CGPoint translation = [recognizer translationInView:button];

button.center = CGPointMake(button.center.x + translation.x, button.center.y + translation.y);
[recognizer setTranslation:CGPointZero inView:button];
}

- (IBAction)buttonWasTapped:(id)sender {
NSLog(@"%s - button tapped",__FUNCTION__);
}

Drag and drop UIButton but limit to boundaries

So let's say you're in the middle of the drag operation. You're moving the button instance around by setting its center to the center of whatever gesture is causing the movement.

You can impose restrictions by testing the gesture's center and resetting the center values if you don't like them. The below assumes a button wired to an action for all Touch Drag events but the principle still applies if you're using gesture recognizers or touchesBegan: and friends.

- (IBAction)handleDrag:(UIButton *)sender forEvent:(UIEvent *)event 
{
CGPoint point = [[[event allTouches] anyObject] locationInView:self.view];

if (point.y > 200)
{
point.y = 200; //No dragging this button lower than 200px from the origin!
}

sender.center = point;
}

If you want a button that slides only on one axis, that's easy enough:

- (IBAction)handleDrag:(UIButton *)sender forEvent:(UIEvent *)event 
{
CGPoint point = [[[event allTouches] anyObject] locationInView:self.view];
point.y = sender.center.y; //Always stick to the same y value

sender.center = point;
}

Or perhaps you want the button draggable only inside the region of a specific view. This might be easier to define if your boundaries are complicated.

- (IBAction)handleDrag:(UIButton *)sender forEvent:(UIEvent *)event 
{
CGPoint point = [[[event allTouches] anyObject] locationInView:self.someView];

if ([self.someView pointInside:point withEvent:nil])
{
sender.center = point;
//Only if the gesture center is inside the specified view will the button be moved
}
}

My UIButton is starting in a random position instead of starting in the center and moving

As @luk2302 points out in his comment, you can't rely on view frames being correct in viewDidLoad. You need to move that logic to viewDidLayoutSubviews.

However, note that if anything causes the system to invoke viewDidLayoutSubviews, your button will snap back to it's original position.

If you want it to stay in it's randomized position once it's moved there, you should add a "wasRandomized" boolean instance variable, and make the logic in viewDidLayoutSubviews only move the button to the center if you haven't randomized it already.

Also note that you should really be using layout constraints. Create a layout constraint for the x position and another one for y, and modify their constant values when you want to move the button. That way auto-layout won't move your button position out from under you.

Correct Way To Move UIButton Between Two Points?

viewDidLoad occurs before the view is visible so it will move just not animated. You could try putting it in the viewDidAppear method.

You can wrap things in UIView animation blocks like so if you want them to animate instead of blink into position.

[UIView animateWithDuration:0.5 animations:^{
button.center = CGPointMake(200.0, 200.0);
}];

Since you're using interface builder I'm assuming your using properties to keep track of the buttons. This can work if you're only using a few set buttons but I'd expect in a game those will change so you may want to move to code or atleast use IBOutletCollections to keep track of the buttons.

Swift 3.0

UIView.animate(withDuration: 0.5) { [weak self] in
self?.button.center = CGPoint(x: 200, y: 200)
}

Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsets

I agree the documentation on imageEdgeInsets and titleEdgeInsets should be better, but I figured out how to get the correct positioning without resorting to trial and error.

The general idea is here at this question, but that was if you wanted both text and image centered. We don't want the image and text to be centered individually, we want the image and the text to be centered together as a single entity. This is in fact what UIButton already does so we simply need to adjust the spacing.

CGFloat spacing = 10; // the amount of spacing to appear between image and title
tabBtn.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, spacing);
tabBtn.titleEdgeInsets = UIEdgeInsetsMake(0, spacing, 0, 0);

I also turned this into a category for UIButton so it will be easy to use:

UIButton+Position.h

@interface UIButton(ImageTitleCentering)

-(void) centerButtonAndImageWithSpacing:(CGFloat)spacing;

@end

UIButton+Position.m

@implementation UIButton(ImageTitleCentering)

-(void) centerButtonAndImageWithSpacing:(CGFloat)spacing {
self.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, spacing);
self.titleEdgeInsets = UIEdgeInsetsMake(0, spacing, 0, 0);
}

@end

So now all I have to do is:

[button centerButtonAndImageWithSpacing:10];

And I get what I need every time. No more messing with the edge insets manually.

EDIT: Swapping Image and Text

In response to @Javal in comments

Using this same mechanism, we can swap the image and the text. To accomplish the swap, simply use a negative spacing but also include the width of the text and the image. This will require frames to be known and layout performed already.

[self.view layoutIfNeeded];
CGFloat flippedSpacing = -(desiredSpacing + button.currentImage.size.width + button.titleLabel.frame.size.width);
[button centerButtonAndImageWithSpacing:flippedSpacing];

Of course you will probably want to make a nice method for this, potentially adding a second category method, this is left as an exercise to the reader.



Related Topics



Leave a reply



Submit