Add boundaries to a UIPangesture - ios

I have a UILabel in a subview and I have a panGesture and pinchGesture on the UILabel. As of right now I can move the UILabel crossed all views. I want this UILabel do stay within the area of the subView. How would I accomplish this?
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
CGPoint translation = [panGesture translationInView:panGesture.view.superview];
if (UIGestureRecognizerStateBegan == panGesture.state ||UIGestureRecognizerStateChanged == panGesture.state) {
panGesture.view.center = CGPointMake(panGesture.view.center.x + translation.x,
panGesture.view.center.y + translation.y);
[panGesture setTranslation:CGPointZero inView:self.view];
}
}
In this line,
CGPoint translation = [panGesture translationInView:panGesture.view.superview];
It is setting it to the superView and I am trying to set it to my subView but I can't seem to figure it out.

Here is my code to handle draggable button and restrict it to the main view boundary
I hope this code will help you
CGPoint translation = [recognizer translationInView:self.view];
CGRect recognizerFrame = recognizer.view.frame;
recognizerFrame.origin.x += translation.x;
recognizerFrame.origin.y += translation.y;
// Check if UIImageView is completely inside its superView
if (CGRectContainsRect(self.view.bounds, recognizerFrame)) {
recognizer.view.frame = recognizerFrame;
}
// Else check if UIImageView is vertically and/or horizontally outside of its
// superView. If yes, then set UImageView's frame accordingly.
// This is required so that when user pans rapidly then it provides smooth translation.
else {
// Check vertically
if (recognizerFrame.origin.y < self.view.bounds.origin.y) {
recognizerFrame.origin.y = 0;
}
else if (recognizerFrame.origin.y + recognizerFrame.size.height > self.view.bounds.size.height) {
recognizerFrame.origin.y = self.view.bounds.size.height - recognizerFrame.size.height;
}
// Check horizantally
if (recognizerFrame.origin.x < self.view.bounds.origin.x) {
recognizerFrame.origin.x = 0;
}
else if (recognizerFrame.origin.x + recognizerFrame.size.width > self.view.bounds.size.width) {
recognizerFrame.origin.x = self.view.bounds.size.width - recognizerFrame.size.width;
}
}
// Reset translation so that on next pan recognition
// we get correct translation value
[recognizer setTranslation:CGPointZero inView:self.view];

Related

iOS, can UIPanGesture be used to do UIImageView corner drag for resizing?

I've searched for some time (days) for a solution, but none really do what I need. (iOS, Objective C, BTW).
I have a UIImageView that I resize with a UIPanGestureRecognizer. The typical pan works fine. It seems like I am so close.
But I want to resize the ImageView by dragging a corner of the image and only resizing dimensions relevant to the selected corner. It works great if I only do my "handleResize" UIPanGesture method. But if I pinch or rotate the image, the bounds or frame get messed up. I think I need some sort of CGAffineTransform but I have not been able to get it to work.
I need help to point me in the right direction. I've been working with CGAffineTransforms but I may be on the wrong track.
In my ViewController.h I have a float, touchRadius, set to 25:
float touchRadius = 25;
I have a UIPanGestureRecognizer in my ViewController.m:
- (IBAction)handleResize:(UIPanGestureRecognizer *)recognizer {
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged) {
CGRect frame = recognizer.view.frame;
CGRect topLeft = CGRectMake(frame.origin.x,
frame.origin.y,
touchRadius, touchRadius);
CGRect bottomLeft = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
CGRect topRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y,
touchRadius, touchRadius);
CGRect bottomRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
Boolean useNewFrame = YES;
CGRect newFrame = frame;
if (CGRectContainsPoint(topLeft, touch)) {
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
newFrame.size.width -= translation.x;
newFrame.size.height -= translation.y;
recognizer.view.frame = newFrame;
} else if (CGRectContainsPoint(topRight, touch)) {
newFrame.origin.y += translation.y;
newFrame.size.width += translation.x;
newFrame.size.height -= translation.y;
recognizer.view.frame = newFrame;
} else if (CGRectContainsPoint(bottomLeft, touch)) {
newFrame.origin.x += translation.x;
newFrame.size.width -= translation.x;
newFrame.size.height += translation.y;
recognizer.view.frame = newFrame;
} else if (CGRectContainsPoint(bottomRight, touch)) {
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
recognizer.view.frame = newFrame;
} else {
useNewFrame = NO;
}
if (useNewFrame) {
// make sure it doesn't go too small to touch
if (newFrame.size.width < touchRadius)
newFrame.size.width = touchRadius;
if (newFrame.size.height < touchRadius)
newFrame.size.height = touchRadius;
recognizer.view.frame = newFrame;
// I THINK I NEED A TRANSFORM HERE
} else {
// use the fallback translate
[recognizer.view setTransform:CGAffineTransformTranslate(recognizer.view.transform, translation.x, translation.y)];
}
}
[recognizer setTranslation:CGPointZero inView:self.view];
}
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged)
{
// make sure it stays visible
float scale = recognizer.scale;
if (recognizer.view.frame.size.width * scale > touchRadius * 2 ||
recognizer.view.frame.size.height * scale > touchRadius * 2) {
[recognizer.view setTransform:CGAffineTransformScale(recognizer.view.transform, scale, scale)];
recognizer.scale = 1;
}
}
}
- (void)handleRotate:(UIRotationGestureRecognizer *)recognizer {
UIGestureRecognizerState state = [recognizer state];
if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged)
{
CGFloat rotation = [recognizer rotation];
[recognizer.view setTransform:CGAffineTransformRotate(recognizer.view.transform, rotation)];
}
[recognizer setRotation:0];
}
Do I need a transform after I modify the view frame? Or should I pursue another path?
I'm trying to do this natively without using libs from others so I understand it. This simplistic example will be part of a larger project.
After some research I figured out how to modify the transform for a corner/side dragging pan, to resize an image. Here are the key elements to make it work:
Save the CGAffineTransform in each gesture (pan, pinch, rotate) if
the recognizer state = UIGestureRecognizerStateBegan
Apply new CGAAffineTransforms on the saved initial transform in each gesture (pan, pinch, rotate)
Save the corner/side detected in the UIGestureRecognizerStateBegan state
Clear the corner/side detected in the UIGestureRecognizerStateEnded state
For pan gesture, adjust the translation x/y values based on corner/side detected
Make sure touch radius is large enough to be useful (24 was too small, 48 works well)
The transform worked like this:
// pan the image
recognizer.view.transform = CGAffineTransformTranslate(initialTransform, tx, ty);
if (scaleIt) {
// the origin or size changed
recognizer.view.frame = newFrame;
}
The tx and ty values were the defaults returned from the recognizer if the pan was from the center of the image. But if the user touch was near a corner or side of the view frame, the tx/ty and the frame origin are adjusted to resize the view making it appear as if that corner or side were being dragged to resize the view.
For example:
CGRect newFrame = recognizer.view.frame;
if (currentDragType == DRAG_TOPLEFT) {
tx = -translation.x;
ty = -translation.y;
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
newFrame.size.width -= translation.x;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_TOPRIGHT) {
tx = translation.x;
ty = -translation.y;
newFrame.origin.y += translation.y;
newFrame.size.width += translation.x;
newFrame.size.height -= translation.y;
}
This makes the top left corner or top right corner move in or out according to how far the touch moved. Unlike a center pan, where the whole view moves along with the touch, the opposite corner (or side) remains fixed.
There were 2 problems I did not resolve:
if the image is rotated significantly, the corner/side detection (pan) does not work because I did not check for the touch in the rotated coordinate system (but the center pan still works fine)
once the image is rotated, pinch gestures work erratically and can resize an image to zero, making it invisible
I created a simple demo and uploaded it to GitHub: https://github.com/ByteSlinger/ImageGestureDemo
Yes, I know that link could go away someday, so here's the code:
ViewController.h
//
// ViewController.h
// ImageGestureDemo
//
// Created by ByteSlinger on 6/21/18.
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import <UIKit/UIKit.h>
NSString *APP_TITLE = #"Image Gesture Demo";
NSString *INTRO_ALERT = #"\nDrag, Pinch and Rotate the Image!"
"\n\nYou can also Drag, Pinch and Rotate the background image."
"\n\nDouble tap an image to reset it";
float touchRadius = 48; // max distance from corners to touch point
typedef NS_ENUM(NSInteger, DragType) {
DRAG_OFF,
DRAG_ON,
DRAG_CENTER,
DRAG_TOP,
DRAG_BOTTOM,
DRAG_LEFT,
DRAG_RIGHT,
DRAG_TOPLEFT,
DRAG_TOPRIGHT,
DRAG_BOTTOMLEFT,
DRAG_BOTTOMRIGHT
};
#interface ViewController : UIViewController <UIGestureRecognizerDelegate>
//callback to process gesture events
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer;
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer;
- (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
#end
ViewController.m
//
// ViewController.m
// ImageGestureDemo
//
// This is a DEMO. It shows how to pan, pinch, rotate and drag/resize a UIImageView.
//
// There is a background image and a foreground image. Both images can be
// panned, pinched and rotated, but only the foreground image can be resized
// by dragging one of it's corners or it's sides.
//
// NOTE: Sure, much of this code could have been put into a subclass of UIView
// or UIImageView. But for simplicity and reference sake, all code and
// methods are in one place, this ViewController subclass. There is no
// error checking at all. App tested on an iPhone 6+ and an iPad gen3.
//
// Features:
// - allows an image to be resized with pan gesture by dragging corners and sides
// - background image can be modified (pan, pinch, rotate)
// - foreground image can be modified (pan, pinch, rotate, drag/resize)
// - all image manipulation done within gestures linked from storyboard
// - all finger touches on screen show with yellow circles
// - when dragging, the touch circles turn to red (so you know when gestures start)
// - double tap on foreground image resets it to original size and rotation
// - double tap on background resets it and also resets the foreground image
// - screen and image touch and size info displayed on screen
// - uses CGAffineTransform objects for image manipulation
// - uses UIGestureRecognizerStateBegan in gestures to save transforms (the secret sauce...)
//
// Known Issues:
// - when the image is rotated, determining if a touch is on a corner or side
// does not work for large rotations. Need to check touch points against
// non rotated view frame and adjust accordingly.
// - after rotations, pinch and resize can shrink image to invisibility despite
// code attempts to prevent it.
//
// Created by ByteSlinger on 6/21/18.
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
#interface ViewController ()
#property (strong, nonatomic) IBOutlet UIImageView *backgroundImageView;
#property (strong, nonatomic) IBOutlet UIImageView *foregroundImageView;
#property (strong, nonatomic) IBOutlet UILabel *screenInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *touchInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *imageInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *backgroundInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *changeInfoLabel;
#property (strong, nonatomic) IBOutlet UITapGestureRecognizer *backgroundTapGesture;
#property (strong, nonatomic) IBOutlet UITapGestureRecognizer *foregroundTapGesture;
#end
#implementation ViewController
CGRect originalImageFrame;
CGRect originalBackgroundFrame;
CGAffineTransform originalImageTransform;
CGAffineTransform originalBackgroundTransform;
NSMutableArray* touchCircles = nil;
DragType currentDragType = DRAG_OFF;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// set this to whatever your desired touch radius is
touchRadius = 48;
// In Storyboard this must have set to 1, then this seems to work ok
// when setting the double tap here
_foregroundTapGesture.numberOfTapsRequired = 2;
_backgroundTapGesture.numberOfTapsRequired = 2;
[self centerImageView:_foregroundImageView];
originalImageFrame = _foregroundImageView.frame;
originalBackgroundFrame = _backgroundImageView.frame;
originalImageTransform = _foregroundImageView.transform;
originalBackgroundTransform = _backgroundImageView.transform;
_backgroundImageView.contentMode = UIViewContentModeCenter;
_foregroundImageView.contentMode = UIViewContentModeScaleToFill; // allow stretch
[_backgroundImageView setUserInteractionEnabled:YES];
[_backgroundImageView setMultipleTouchEnabled:YES];
[_foregroundImageView setUserInteractionEnabled:YES];
[_foregroundImageView setMultipleTouchEnabled:YES];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:[UIDevice currentDevice]];
[_touchInfoLabel setText:nil];
[_changeInfoLabel setText:nil];
[_imageInfoLabel setText:nil];
[_backgroundInfoLabel setText:nil];
touchCircles = [[NSMutableArray alloc] init];
}
- (void)viewDidAppear:(BOOL)animated {
[self alert:APP_TITLE :INTRO_ALERT];
}
- (void) orientationChanged:(NSNotification *)note
{
UIDevice * device = note.object;
switch(device.orientation)
{
case UIDeviceOrientationPortrait:
/* start special animation */
break;
case UIDeviceOrientationPortraitUpsideDown:
/* start special animation */
break;
default:
break;
};
[_screenInfoLabel setText:[NSString stringWithFormat:#"Screen: %.0f/%.0f",
self.view.frame.size.width,self.view.frame.size.height]];
}
//
// Update the info labels from the passed objects
//
- (void) updateInfo:(UIView *)imageView touch:(CGPoint)touch change:(CGPoint)change {
NSString *label;
UILabel *infoLabel;
if (imageView == _foregroundImageView) {
label = #"Image: %0.f/%0.f, %0.f/%0.f";
infoLabel = _imageInfoLabel;
} else {
label = #"Background: %0.f/%0.f, %0.f/%0.f";
infoLabel = _backgroundInfoLabel;
}
[infoLabel setText:[NSString stringWithFormat:label,
imageView.layer.frame.origin.x,
imageView.layer.frame.origin.y,
imageView.layer.frame.size.width,
imageView.layer.frame.size.height]];
[_touchInfoLabel setText:[NSString stringWithFormat:#"Touch: %0.f/%.0f",
touch.x,touch.y]];
[_changeInfoLabel setText:[NSString stringWithFormat:#"Change: %0.f/%.0f",
change.x,change.y]];
}
//
// Center the passed image frame within it's bounds
//
- (void)centerImageView:(UIImageView *)imageView {
CGSize boundsSize = self.view.bounds.size;
CGRect frameToCenter = imageView.frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
else
frameToCenter.origin.x = 0;
// center vertically
if (frameToCenter.size.height < boundsSize.height)
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
else
frameToCenter.origin.y = 0;
imageView.frame = frameToCenter;
}
//
// Remove all touch circles
//
- (void)removeTouchCircles {
[touchCircles makeObjectsPerformSelector: #selector(removeFromSuperview)];
[touchCircles removeAllObjects];
}
//
// Draw a circle around the passed point where the user has touched the screen
//
- (void)drawTouchCircle:(UIView *)view fromCenter:(CGPoint)point ofRadius:(float)radius {
CGRect frame = CGRectMake(point.x - view.frame.origin.x - radius,
point.y - view.frame.origin.y - radius,
radius * 2, radius * 2);
UIView *circle = [[UIView alloc] initWithFrame:frame];
circle.alpha = 0.5;
circle.layer.cornerRadius = radius;
circle.backgroundColor = currentDragType == DRAG_OFF ? [UIColor yellowColor] : [UIColor redColor];
[circle.layer setBorderWidth:1.0];
[circle.layer setBorderColor:[[UIColor blackColor]CGColor]];
[view addSubview:circle];
[touchCircles addObject:circle];
}
//
// Draw a touch circle for the passed user touch
//
- (void)handleTouchEvent:(UIView *) view
atPoint:(CGPoint) point
forState:(UIGestureRecognizerState) state
clear:(Boolean) clear {
//NSLog(#"handleTouchEvent");
if (clear) {
[self removeTouchCircles];
}
if (state == UIGestureRecognizerStateEnded) {
[self removeTouchCircles];
} else {
[self drawTouchCircle:self.view fromCenter:point ofRadius:touchRadius];
}
[_touchInfoLabel setText:[NSString stringWithFormat:#"Touch: %0.f/%.0f",
point.x,point.y]];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesBegan");
[self removeTouchCircles];
NSSet *allTouches = [event allTouches];
NSArray *allObjects = [allTouches allObjects];
for (int i = 0;i < [allObjects count];i++)
{
UITouch *touch = [allObjects objectAtIndex:i];
CGPoint location = [touch locationInView: self.view];
[self handleTouchEvent:touch.view atPoint:location forState:UIGestureRecognizerStateBegan clear:NO];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesMoved");
[self removeTouchCircles];
NSSet *allTouches = [event allTouches];
NSArray *allObjects = [allTouches allObjects];
for (int i = 0;i < [allObjects count];i++)
{
UITouch *touch = [allObjects objectAtIndex:i];
CGPoint location = [touch locationInView: self.view];
[self handleTouchEvent:touch.view atPoint:location forState:UIGestureRecognizerStateChanged clear:NO];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesEnded");
[self removeTouchCircles];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesCancelled");
[self removeTouchCircles];
}
//
// Double tap resets passed image. If background image, also reset foreground image.
//
- (IBAction)handleDoubleTap:(UITapGestureRecognizer *)recognizer {
CGPoint touch = [recognizer locationInView: self.view];
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged) {
[self handleTouchEvent:recognizer.view atPoint:touch forState:recognizer.state clear:NO];
} else {
[self removeTouchCircles];
}
[self alert:#"Reset" :#"The Image has been Reset!"];
CGRect frame = originalImageFrame;
CGAffineTransform transform = originalImageTransform;
if (recognizer.view == _backgroundImageView) {
_foregroundImageView.transform = transform;
_foregroundImageView.frame = frame;
[self updateInfo:_foregroundImageView touch:touch change:CGPointZero];
frame = originalBackgroundFrame;
transform = originalBackgroundTransform;
}
recognizer.view.transform = transform;
recognizer.view.frame = frame;
[self updateInfo:recognizer.view touch:touch change:CGPointZero];
}
- (void) setDragType:(CGRect)frame withTouch:(CGPoint)touch {
// the corners and sides of the current view frame
CGRect topLeft = CGRectMake(frame.origin.x,frame.origin.y,
touchRadius, touchRadius);
CGRect bottomLeft = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
CGRect topRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y,
touchRadius, touchRadius);
CGRect bottomRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
CGRect leftSide = CGRectMake(frame.origin.x,frame.origin.y,
touchRadius, frame.size.height);
CGRect rightSide = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y,
touchRadius, frame.size.height);
CGRect topSide = CGRectMake(frame.origin.x,frame.origin.y,
frame.size.width, touchRadius);
CGRect bottomSide = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height - touchRadius,
frame.size.width, touchRadius);
if (CGRectContainsPoint(topLeft, touch)) {
currentDragType = DRAG_TOPLEFT;
} else if (CGRectContainsPoint(topRight, touch)) {
currentDragType = DRAG_TOPRIGHT;
} else if (CGRectContainsPoint(bottomLeft, touch)) {
currentDragType = DRAG_BOTTOMLEFT;
} else if (CGRectContainsPoint(bottomRight, touch)) {
currentDragType = DRAG_BOTTOMRIGHT;
} else if (CGRectContainsPoint(topSide, touch)) {
currentDragType = DRAG_TOP;
} else if (CGRectContainsPoint(bottomSide, touch)) {
currentDragType = DRAG_BOTTOM;
} else if (CGRectContainsPoint(leftSide, touch)) {
currentDragType = DRAG_LEFT;
} else if (CGRectContainsPoint(rightSide, touch)) {
currentDragType = DRAG_RIGHT;
} else if (CGRectContainsPoint(frame, touch)) {
currentDragType = DRAG_CENTER;
} else {
currentDragType = DRAG_OFF; // touch point is not in the view frame
}
}
//
// Return the unrotated size of the view
//
- (CGSize) getActualSize:(UIView *)view {
CGSize result;
//CGSize originalSize = view.frame.size;
CGAffineTransform originalTransform = view.transform;
float rotation = atan2f(view.transform.b, view.transform.a);
// reverse rotation of current transform
CGAffineTransform unrotated = CGAffineTransformRotate(view.transform, -rotation);
view.transform = unrotated;
// get the size of the "unrotated" view
result = view.frame.size;
// reset back to what it was
view.transform = originalTransform;
//NSLog(#"Size current = %0.f/%0.f, rotation = %0.2f, unrotated = %0.f/%0.f",
// originalSize.width,originalSize.height,
// rotation,
// result.width,result.height);
return result;
}
//
// Resize or Pan an image on the ViewController View
//
- (IBAction)handleResize:(UIPanGestureRecognizer *)recognizer {
static CGRect initialFrame;
static CGAffineTransform initialTransform;
static Boolean scaleIt = YES;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialFrame = recognizer.view.frame;
initialTransform = recognizer.view.transform;
[self setDragType:recognizer.view.frame withTouch:touch];
scaleIt = YES;
}
if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
scaleIt = NO;
[self getActualSize:recognizer.view];
} else {
// our new view frame - start with the initial one
CGRect newFrame = initialFrame;
// adjust the translation point according to where the user touched the image
float tx = translation.x;
float ty = translation.y;
// resize by dragging a corner or a side
if (currentDragType == DRAG_TOPLEFT) {
tx = -translation.x;
ty = -translation.y;
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
newFrame.size.width -= translation.x;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_TOPRIGHT) {
ty = -translation.y;
newFrame.origin.y += translation.y;
newFrame.size.width += translation.x;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_BOTTOMLEFT) {
tx = -translation.x;
newFrame.origin.x += translation.x;
newFrame.size.width -= translation.x;
newFrame.size.height += translation.y;
} else if (currentDragType == DRAG_BOTTOMRIGHT) {
// origin does not change
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
} else if (currentDragType == DRAG_TOP) {
tx = 0;
newFrame.origin.y += translation.y;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_BOTTOM) {
tx = 0;
newFrame.size.height += translation.y;
} else if (currentDragType == DRAG_LEFT) {
tx = -translation.x;
ty = 0;
newFrame.origin.x += translation.x;
newFrame.size.width -= translation.x;
} else if (currentDragType == DRAG_BOTTOM) {
ty = 0;
newFrame.size.width += translation.x;
} else { //if (currentDragType == DRAG_CENTER) {
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
scaleIt = NO; // normal pan
}
// get the unrotated size of the view
CGSize actualSize = [self getActualSize:recognizer.view];
// make sure we can still touch the image
if (actualSize.width < touchRadius * 2) {
newFrame.size.width += touchRadius * 2;
tx = 0; // stop resizing
}
if (actualSize.height < touchRadius * 2) {
newFrame.size.height += touchRadius * 2;
ty = 0; // stop resizing
}
// pan the image
recognizer.view.transform = CGAffineTransformTranslate(initialTransform, tx, ty);
if (scaleIt) {
// the origin or size changed
recognizer.view.frame = newFrame;
}
}
[self updateInfo:recognizer.view touch:touch change:translation];
}
//
// Pan an image on the ViewController View
//
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialTransform = recognizer.view.transform;
currentDragType = DRAG_ON;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
[self getActualSize:recognizer.view];
}
recognizer.view.transform = CGAffineTransformTranslate(initialTransform, translation.x, translation.y);
[self updateInfo:recognizer.view touch:touch change:translation];
}
//
// Pinch (resize) an image on the ViewController View
//
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGSize initialSize;
static CGAffineTransform initialTransform;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint change = CGPointMake(recognizer.view.transform.tx, recognizer.view.transform.ty);
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialSize = recognizer.view.frame.size;
initialTransform = recognizer.view.transform;
currentDragType = DRAG_ON;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
[self getActualSize:recognizer.view];
}
// make sure it stays visible
float scale = recognizer.scale;
float newWidth = initialSize.width * scale;
float newHeight = initialSize.height * scale;
// make sure we can still touch it
if (newWidth > touchRadius * 2 && newHeight > touchRadius * 2) {
// scale the image
recognizer.view.transform = CGAffineTransformScale(initialTransform, scale, scale);
}
[self updateInfo:recognizer.view touch:touch change:change];
}
//
// Rotate an image on the ViewController View
//
- (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer {
static CGFloat initialRotation;
static CGAffineTransform initialTransform;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialTransform = recognizer.view.transform;
initialRotation = atan2f(recognizer.view.transform.b, recognizer.view.transform.a);
currentDragType = DRAG_ON;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
[self getActualSize:recognizer.view];
}
recognizer.view.transform = CGAffineTransformRotate(initialTransform, recognizer.rotation);
[self updateInfo:recognizer.view touch:touch change:CGPointMake(initialRotation, recognizer.rotation)];
}
//
// Prevent simultaneous gestures so my transforms don't get funky
// (may not be necessary ... )
//
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return NO;
}
//
// Spew a message to the user
//
- (void)alert:(NSString *) title :(NSString *)message {
UIAlertController *alert = [UIAlertController
alertControllerWithTitle: title
message: message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okButton = [UIAlertAction
actionWithTitle:#"Ok"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
}];
[alert addAction:okButton];
[self presentViewController:alert animated:YES completion:nil];
}
#end

How to restrict a moveable view by Pan gesture

I have a UIImageView which is moveable via a pan gesture.
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.photoMask addGestureRecognizer:pan];
I would like to restrict the area this can be moved on screen. Rather than the user be able to drag the view right to the side of the screen, I want to restrict it by a margin of some sort. How can I do this?
Also, how is this then handled when rotated?
EDIT ---
#pragma mark - Gesture Recognizer
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
NSLog(#"Pan Gesture");
gesture.view.center = [gesture locationInView:self.view];
}
This is my current method to handle the pan. What I need to do is continue to move the imageview by the center point and also restrict its movement when close to the edge of the screen by 50 for example.
One possible solution to this is in your handlePan method, check the location of the point on the screen, and only commit the change if it is within the bounds you wish to restrict it to.
For ex.
-(void) handlePan:(UIGestureRecognizer*)panGes{
CGPoint point = [panGes locationInView:self.view];
//Only allow movement up to within 100 pixels of the right bound of the screen
if (point.x < [UIScreen mainScreen].bounds.size.width - 100) {
CGRect newframe = CGRectMake(point.x, point.y, theImageView.frame.size.width, theImageView.frame.size.height);
theImageView.frame = newframe;
}
}
I believe this would also correctly handle any screen rotation
EDIT
To move your image view by the center of its frame, the handlePan method could look something like this.
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.view];
//Only allow movement up to within 50 pixels of the bounds of the screen
//Ex. (IPhone 5)
CGRect boundsRect = CGRectMake(50, 50, 220, 448);
if (CGRectContainsPoint(boundsRect, point)) {
imgView.center = point;
}
}
Check whether the point is within your desired bounds, and if so, set the center of your image view frame to that point.
I'm not sure if I'm being over-simplistic here but I think you can accomplish this by using an if clause.
-(void)handlePan:(UIPanGestureRecognizer*)gesture {
UIImageView *viewToDrag = gesture.view; // this is the view you want to move
CGPoint translation = [gesture translationInView:viewToDrag.superview]; // get the movement delta
CGRect movedFrame = CGRectOffset(viewToDrag.frame, translation.x, translation.y); // this is the new (moved) frame
// Now this is the critical part because I don't know if your "margin"
// is a CGRect or maybe some int values, the important thing here is
// to compare if the "movedFrame" values are in the allowed movement area
// Assuming that your margin is a CGRect you could do the following:
if (CGRectContainsRect(yourPermissibleMargin, movedFrame)) {
CGPoint newCenter = CGPointMake(CGRectGetMidX(movedFrame), CGRectGetMidY(movedFrame));
viewToDrag.center = newCenter; // Move your view
}
// -OR-
// If you have your margins as int values you could do the following:
if ( (movedFrame.origin.x + movedFrame.size.width) < 50) {
CGPoint newCenter = CGPointMake(CGRectGetMidX(movedFrame), CGRectGetMidY(movedFrame));
viewToDrag.center = newCenter; // Move your view
}
}
You'll probably have to adapt this to meet your specific needs.
Hope this helps!
Here is the answer in Swift 4 -
Restrict the view's movement to superview
#objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer)
{
// Allows smooth movement of stickers.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed
{
let point = gestureRecognizer.location(in: self.superview)
if let superview = self.superview
{
let restrictByPoint : CGFloat = 30.0
let superBounds = CGRect(x: superview.bounds.origin.x + restrictByPoint, y: superview.bounds.origin.y + restrictByPoint, width: superview.bounds.size.width - 2*restrictByPoint, height: superview.bounds.size.height - 2*restrictByPoint)
if (superBounds.contains(point))
{
let translation = gestureRecognizer.translation(in: self.superview)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.superview)
}
}
}
}
If you want more control over it, match restrictByPoint value to your movable view's frame.
- (void)dragAction:(UIPanGestureRecognizer *)gesture{
UILabel *label = (UILabel *)gesture.view;
CGPoint translation = [gesture translationInView:label];
if (CGRectContainsPoint(label.frame, [gesture locationInView:label] )) {
label.center = CGPointMake(label.center.x,
label.center.y);
[gesture setTranslation:CGPointZero inView:label];
}
else{
label.center = CGPointMake(label.center.x,
label.center.y + translation.y);
[gesture setTranslation:CGPointZero inView:label];
}
}

Pan gesture behaving weird after default rotation and scale

I am currently designing a little game as a learning project and it is basically the following, a image is rotated and scaled on viewDidLoad and another image is a direct copy of the original image.
So basically there is a image that is a little bit different from the other, the objective is to scale it back down, rotate it and move it on top of the other image with 5 pixels, 5 degrees of rotate and 5 percent scale.
I have run into an issue. I "skew" the image using the following code...
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI/2.5);
image.transform = CGAffineTransformScale(transform, 1.25, 1.25);
My pan gesture does not perform correctly after rotating the image and then scaling it 125%.
Does anyone know what could be going on here? By incorrectly I mean that it doesn't move around with my finger.. It seems to glide or go the opposite direction. Video .
My pan gesture method is below.
if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [gesture translationInView:image];
//if within game field
if((image.center.x + translation.x) > 50.0 && (image.center.x + translation.x) < 255.0 && (image.center.y + translation.y) > 50.0 && (image.center.y + translation.y) < 302) {
[image setCenter:CGPointMake([image center].x + translation.x, [image center].y + translation.y)]; //move it
}
}
[gesture setTranslation:CGPointZero inView:[image superview]];
if(gesture.state == UIGestureRecognizerStateEnded) [self didWin]; // not relevant to question
Does anyone know why pan performs incorrectly after I rotate and scale my image? When I comment out those first two lines of code the pan performs correctly and moves around with the users finger.
Thanks in advance for any suggestions or help!
Rotation might have changed the frame. I used cosf and sinf function to deal with it.
handlePan and handleRotate are the callback functions, in which I can control the subview of self.view. Here, you should replace image with your own view.
static CGFloat _rotation = 0;
- (void)handlePan:(UIPanGestureRecognizer*)recognizer
{
UIImageView *image = nil;
for (UIImageView *tmp in recognizer.view.subviews) { // pick the subview
if (tmp.tag == AXIS_TAG) {
image = tmp;
}
}
CGPoint translation = [recognizer translationInView:image];
// I have found any related documents yet, but these equations do work!//////
CGFloat dx = translation.x * cosf(_rotation) - translation.y*sinf(_rotation);
CGFloat dy = translation.x * sinf(_rotation) + translation.y*cosf(_rotation);
/////////////////////////////////////////////////////////////////////////////
image.center = CGPointMake(image.center.x+dx, dy+image.center.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:image];
}
- (void)handleRotate:(UIRotationGestureRecognizer*)recognizer
{
UIImageView *image = nil;
for (UIImageView *tmp in recognizer.view.subviews) { // pick the subview
if (tmp.tag == AXIS_TAG) {
image = tmp;
}
}
CGFloat r = [recognizer rotation];
if ((recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged)
&& recognizer.numberOfTouches == 2) {
image.transform = CGAffineTransformMakeRotation(_rotation+r);
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
_rotation+=r; // Record the final rotation
}
}
The solution was to change the pan code just a tiny bit..
if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [gesture translationInView:self.view]; //CHANGED
//if within game field
if((image.center.x + translation.x) > 50.0 && (image.center.x + translation.x) < 255.0 && (image.center.y + translation.y) > 50.0 && (image.center.y + translation.y) < 302) {
[image setCenter:CGPointMake([image center].x + translation.x, [image center].y + translation.y)]; //move it
}
}
[gesture setTranslation:CGPointZero inView:self.view];
I changed in view:self.view and translationInView:self.view];

How to limit a view panning just inside an other View?

I have a subview (the red bar) laid inside a view. I want to limit the panning area inside the parent view and only absolutely horizontal or vertical. And, of course not outside of the parent view.
I did try some code that handle the panning event, but sometime I can drag the red bar out. Any suggestion?
-(void)handleMoveLineView:(UIPanGestureRecognizer *)recognizer{
CGPoint sPoint= [recognizer locationInView:self.groundView];
CGPoint newCenter = [recognizer translationInView:self.groundView];
NSLog(#"X0 = %f, Y0 = %f", newCenter.x, newCenter.y);
if([recognizer state] == UIGestureRecognizerStateBegan) {
beginX = recognizer.view.center.x;
beginY = recognizer.view.center.y;
}
for (UIView *view in viewArrayGroundView) {
if (CGRectContainsPoint(view.frame, sPoint)) {
//Limit not out side parent view
if (beginX + newCenter.x != view.frame.size.width /2) {
newCenter = CGPointMake(view.frame.size.width /2, beginY + newCenter.y);
}
//Limit top - since the redbar height is 3
else if((beginY + newCenter.y < view.frame.origin.y + 3)){
newCenter.y = view.frame.origin.y + 3;
}
//Limit bottom
else if((beginY + newCenter.y > view.frame.size.height - 3)){
newCenter.y = view.frame.origin.y - 3;
}
}
}
[recognizer.view setCenter:newCenter];
NSLog(#"X = %f, Y = %f", newCenter.x, newCenter.y);
}

Moving UIView within Parent UIView (UIPanGestureRecognizer)

I am using the following code to move a UIImageView that is present inside of a UIView.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
I would like to make it so that the UIView does not move outside of the parent view. At the minute the image view is able to move across the entire screen.
First get the new frame of your UIImageView and check if it is completely inside its superView using CGRectContainsRect() method. If yes, then set UImageView's frame to new frame.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
CGRect recognizerFrame = recognizer.view.frame;
recognizerFrame.origin.x += translation.x;
recognizerFrame.origin.y += translation.y;
// Check if UIImageView is completely inside its superView
if (CGRectContainsRect(self.view.bounds, recognizerFrame)) {
recognizer.view.frame = recognizerFrame;
}
// Else check if UIImageView is vertically and/or horizontally outside of its
// superView. If yes, then set UImageView's frame accordingly.
// This is required so that when user pans rapidly then it provides smooth translation.
else {
// Check vertically
if (recognizerFrame.origin.y < self.view.bounds.origin.y) {
recognizerFrame.origin.y = 0;
}
else if (recognizerFrame.origin.y + recognizerFrame.size.height > self.view.bounds.size.height) {
recognizerFrame.origin.y = self.view.bounds.size.height - recognizerFrame.size.height;
}
// Check horizantally
if (recognizerFrame.origin.x < self.view.bounds.origin.x) {
recognizerFrame.origin.x = 0;
}
else if (recognizerFrame.origin.x + recognizerFrame.size.width > self.view.bounds.size.width) {
recognizerFrame.origin.x = self.view.bounds.size.width - recognizerFrame.size.width;
}
}
// Reset translation so that on next pan recognition
// we get correct translation value
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
Make sure that you pass bounds of superView and frame of UIImageView so that both CGRects are in same coordinate system.
Try with:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer
{
if (gesture.state==UIGestureRecognizerStateChanged || gesture.state == UIGestureRecognizerStateEnded){
UIView *superview = recognizer.view.superview;
CGSize superviewSize = superview.bounds.size;
CGSize thisSize = recognizer.view.size;
CGPoint translation = [recognizer translationInView:self.view];
CGPoint center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
CGPoint resetTranslation = CGPointMake(translation.x, translation.y);
if(center.x - thisSize.width/2 < 0)
center.x = thisSize.width/2;
else if (center.x + thisSize.width/2 > superviewSize.width)
center.x = superviewSize.width-thisSize.width/2;
else
resetTranslation.x = 0; //Only reset the horizontal translation if the view *did* translate horizontally
if(center.y - thisSize.height/2 < 0)
center.y = thisSize.height/2;
else if(center.y + thisSize.height/2 > superviewSize.height)
center.y = superviewSize.height-thisSize.height/2;
else
resetTranslation.y = 0; //Only reset the vertical translation if the view *did* translate vertically
recognizer.view.center = center;
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
}
This way it won't ever move outside the parent view bounds and it will just "stick" to the edge if you try to move it out of the bounds!
Swift version of micantox answer
let gesture = UIPanGestureRecognizer(target: self, action: #selector(self.wasDragged(gestureRecognizer:)))
imageView.addGestureRecognizer(gesture)
imageView.isUserInteractionEnabled = true
#objc func wasDragged(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.changed || gestureRecognizer.state == UIGestureRecognizerState.ended {
let superview = gestureRecognizer.view?.superview
let superviewSize = superview?.bounds.size
let thisSize = gestureRecognizer.view?.frame.size
let translation = gestureRecognizer.translation(in: self.view)
var center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
var resetTranslation = CGPoint(x: translation.x, y: translation.y)
if center.x - (thisSize?.width)!/2 < 0 {
center.x = (thisSize?.width)!/2
} else if center.x + (thisSize?.width)!/2 > (superviewSize?.width)! {
center.x = (superviewSize?.width)!-(thisSize?.width)!/2
} else {
resetTranslation.x = 0 //Only reset the horizontal translation if the view *did* translate horizontally
}
if center.y - (thisSize?.height)!/2 < 0 {
center.y = (thisSize?.height)!/2
} else if center.y + (thisSize?.height)!/2 > (superviewSize?.height)! {
center.y = (superviewSize?.height)!-(thisSize?.height)!/2
} else {
resetTranslation.y = 0 //Only reset the vertical translation if the view *did* translate vertically
}
gestureRecognizer.view?.center = center
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
You'll have to set the recognizer.view.center to a new value only if its frame is inside the bounds of its parent view. Use CGRectContainsRect on recognizer.view.superview.bounds and recognizer.view.frame to verify that they are contained.
If you want to allow the image view to move outside its parent view until the center point of the view is outside the parent's bounds, you can use the convertPoint:toView method of UIView and verify that the new CGPoint is not outside your parent's bounds.
if (gesture.state==UIGestureRecognizerStateChanged){
CGPoint translation = [recognizer translationInView:self.view];
CGRect rectToCheck=CGRectMake(yourView.frame.origin.x+translation.x, yourView.frame.origin.y+translation.y, CGRectGetWidth(yourView.frame), CGRectGetHeight(yourView.frame));
if(CGRectContainsRect(self.view.bounds,rectToCheck))// check that the rect that is going to form lies within the bounds of self.view
{
yourView.frame=CGRectMake(yourView.frame.origin.x+translation.x, yourView.frame.origin.y+translation.y, CGRectGetWidth(yourView.frame), CGRectGetHeight(yourView.frame));
}
[gesture setTranslation:CGPointZero yourView]; // important to set this
}
P.S. Use the gesture state block to initiate, pan, remove. i.e. UIGestureRecognizerStateBegan
UIGestureRecognizerStateChanged
UIGestureRecognizerStateEnded

Resources