How would I be able to limit the scale of the UIPinchGestureRecognizer to a min and max level? The scale property below seems to be relative to the last known scale (the de
I took some info gleaned from Paul Solt and Anoime's answers, and added that to an existing category I have made for UIViewController to allow making any UIView draggable, to now make it pinchable using gestures and transforms.
Note: this dirties the tag property of the view you are making draggable/pinchable. So if you needed the tag for something else, you can consider placing that value in the NSMutableDictionary being used by this technique. That's available as [self dictForView:theView]
Implementing in your project:
You can make any subview within the view controllers "view" draggable or pinchable (or both) place a single line of code in your viewDidLoad (for example:)
[self makeView:mySubView draggable:YES pinchable:YES minPinchScale:0.75 maxPinchScale:1.0];
turn it off in viewDidUnload (releases guestures & dictionary):
[self makeView:mySubView draggable:NO pinchable:NO minPinchScale:1.0 maxPinchScale:1.0];
DragAndPinchScale.h file
#import <UIKit/UIKit.h>
@interface UIViewController (DragAndPinchScale)
-(void) makeView:(UIView*)aView
draggable:(BOOL)draggable
pinchable:(BOOL)pinchable
minPinchScale:(CGFloat)minPinchScale
maxPinchScale:(CGFloat)maxPinchScale;
-(NSMutableDictionary *) dictForView:(UIView *)theView;
-(NSMutableDictionary *) dictForViewGuestures:(UIGestureRecognizer *)guesture;
@end
DragAndPinchScale.m file
#import "DragAndPinchScale.h"
@implementation UIViewController (DragAndPinchScale)
-(NSMutableDictionary *) dictForView:(UIView *)theView{
NSMutableDictionary *dict = (NSMutableDictionary*) (void*) theView.tag;
if (!dict) {
dict = [[NSMutableDictionary dictionary ] retain];
theView.tag = (NSInteger) (void *) dict;
}
return dict;
}
-(NSMutableDictionary *) dictForViewGuestures:(UIGestureRecognizer *)guesture {
return [self dictForView:guesture.view];
}
- (IBAction)fingersDidPinchInPinchableView:(UIPinchGestureRecognizer *)fingers {
NSMutableDictionary *dict = [self dictForViewGuestures:fingers];
UIView *viewToZoom = fingers.view;
CGFloat lastScale;
if([fingers state] == UIGestureRecognizerStateBegan) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = [fingers scale];
} else {
lastScale = [[dict objectForKey:@"lastScale"] floatValue];
}
if ([fingers state] == UIGestureRecognizerStateBegan ||
[fingers state] == UIGestureRecognizerStateChanged) {
CGFloat currentScale = [[[fingers view].layer valueForKeyPath:@"transform.scale"] floatValue];
// limits to adjust the max/min values of zoom
CGFloat maxScale = [[dict objectForKey:@"maxScale"] floatValue];
CGFloat minScale = [[dict objectForKey:@"minScale"] floatValue];
CGFloat newScale = 1 - (lastScale - [fingers scale]);
newScale = MIN(newScale, maxScale / currentScale);
newScale = MAX(newScale, minScale / currentScale);
CGAffineTransform transform = CGAffineTransformScale([[fingers view] transform], newScale, newScale);
viewToZoom.transform = transform;
lastScale = [fingers scale]; // Store the previous scale factor for the next pinch gesture call
}
[dict setObject:[NSNumber numberWithFloat:lastScale]
forKey:@"lastScale"];
}
- (void)fingerDidMoveInDraggableView:(UIPanGestureRecognizer *)finger {
NSMutableDictionary *dict = [self dictForViewGuestures:finger];
UIView *viewToDrag = finger.view;
if (finger.state == UIGestureRecognizerStateBegan) {
[dict setObject:[NSValue valueWithCGPoint:viewToDrag.frame.origin]
forKey:@"startDragOffset"];
[dict setObject:[NSValue valueWithCGPoint:[finger locationInView:self.view]]
forKey:@"startDragLocation"];
}
else if (finger.state == UIGestureRecognizerStateChanged) {
NSMutableDictionary *dict = (NSMutableDictionary*) (void*) viewToDrag.tag;
CGPoint stopLocation = [finger locationInView:self.view];
CGPoint startDragLocation = [[dict valueForKey:@"startDragLocation"] CGPointValue];
CGPoint startDragOffset = [[dict valueForKey:@"startDragOffset"] CGPointValue];
CGFloat dx = stopLocation.x - startDragLocation.x;
CGFloat dy = stopLocation.y - startDragLocation.y;
// CGFloat distance = sqrt(dx*dx + dy*dy );
CGRect dragFrame = viewToDrag.frame;
CGSize selfViewSize = self.view.frame.size;
if (!UIDeviceOrientationIsPortrait(self.interfaceOrientation)) {
selfViewSize = CGSizeMake(selfViewSize.height,selfViewSize.width);
}
selfViewSize.width -= dragFrame.size.width;
selfViewSize.height -= dragFrame.size.height;
dragFrame.origin.x = MIN(selfViewSize.width, MAX(0,startDragOffset.x+dx));
dragFrame.origin.y = MIN(selfViewSize.height,MAX(0,startDragOffset.y+dy));
viewToDrag.frame = dragFrame;
}
else if (finger.state == UIGestureRecognizerStateEnded) {
[dict removeObjectForKey:@"startDragLocation"];
[dict removeObjectForKey:@"startDragOffset"];
}
}
-(void) makeView:(UIView*)aView
draggable:(BOOL)draggable
pinchable:(BOOL)pinchable
minPinchScale:(CGFloat)minPinchScale
maxPinchScale:(CGFloat)maxPinchScale{
NSMutableDictionary *dict = (NSMutableDictionary*) (void*) aView.tag;
if (!(pinchable || draggable)) {
if (dict){
[dict release];
aView.tag = 0;
}
return;
}
if (dict) {
UIPanGestureRecognizer *pan =[dict objectForKey:@"UIPanGestureRecognizer"];
if(pan){
if ([aView.gestureRecognizers indexOfObject:pan]!=NSNotFound) {
[aView removeGestureRecognizer:pan];
}
[dict removeObjectForKey:@"UIPanGestureRecognizer"];
}
UIPinchGestureRecognizer *pinch =[dict objectForKey:@"UIPinchGestureRecognizer"];
if(pinch){
if ([aView.gestureRecognizers indexOfObject:pinch]!=NSNotFound) {
[aView removeGestureRecognizer:pinch];
}
[dict removeObjectForKey:@"UIPinchGestureRecognizer"];
}
[dict removeObjectForKey:@"startDragLocation"];
[dict removeObjectForKey:@"startDragOffset"];
[dict removeObjectForKey:@"lastScale"];
[dict removeObjectForKey:@"minScale"];
[dict removeObjectForKey:@"maxScale"];
}
if (draggable) {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(fingerDidMoveInDraggableView:)];
pan.minimumNumberOfTouches = 1;
pan.maximumNumberOfTouches = 1;
[aView addGestureRecognizer:pan];
[pan release];
dict = [self dictForViewGuestures:pan];
[dict setObject:pan forKey:@"UIPanGestureRecognizer"];
}
if (pinchable) {
CGAffineTransform initialTramsform = CGAffineTransformMakeScale(1.0, 1.0);
aView.transform = initialTramsform;
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(fingersDidPinchInPinchableView:)];
[aView addGestureRecognizer:pinch];
[pinch release];
dict = [self dictForViewGuestures:pinch];
[dict setObject:pinch forKey:@"UIPinchGestureRecognizer"];
[dict setObject:[NSNumber numberWithFloat:minPinchScale] forKey:@"minScale"];
[dict setObject:[NSNumber numberWithFloat:maxPinchScale] forKey:@"maxScale"];
}
}
@end
- (void)pinchToZoom:(UIPinchGestureRecognizer*)gesture
{
switch (gesture.state)
{
case UIGestureRecognizerStateBegan:
{
lastScale = gesture.scale;
}break;
case UIGestureRecognizerStateChanged:
{
const CGFloat zoomSensitivity = 5;
const CGFloat zoomMin = 1;
const CGFloat zoomMax = 16;
CGFloat objectScale = gesture.view.contentScaleFactor;
CGFloat zoomDiff = lastScale - gesture.scale;
CGFloat zoomDirty = objectScale - zoomDiff * zoomSensivity;
CGFloat zoomTo = fmaxf(zoomMin, fminf(zoomDirty, zoomMax));
// step round if needed (neutralize elusive changes)
zoomTo = (NSInteger)(zoomTo * 10) * 0.1;
if ( objectScale != zoomTo )
gesture.view.contentScaleFactor = zoomTo;
lastScale = gesture.scale;
}break;
default:
break;
}
}
Here is the solution that I figured out after using Anomie's answer as a starting point.
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)gestureRecognizer {
if([gestureRecognizer state] == UIGestureRecognizerStateBegan) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = [gestureRecognizer scale];
}
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan ||
[gestureRecognizer state] == UIGestureRecognizerStateChanged) {
CGFloat currentScale = [[[gestureRecognizer view].layer valueForKeyPath:@"transform.scale"] floatValue];
// Constants to adjust the max/min values of zoom
const CGFloat kMaxScale = 2.0;
const CGFloat kMinScale = 1.0;
CGFloat newScale = 1 - (lastScale - [gestureRecognizer scale]);
newScale = MIN(newScale, kMaxScale / currentScale);
newScale = MAX(newScale, kMinScale / currentScale);
CGAffineTransform transform = CGAffineTransformScale([[gestureRecognizer view] transform], newScale, newScale);
[gestureRecognizer view].transform = transform;
lastScale = [gestureRecognizer scale]; // Store the previous scale factor for the next pinch gesture call
}
}
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer{
//recognizer.scale=1;
CGFloat pinchScale = recognizer.scale;
pinchScale = round(pinchScale * 1000) / 1000.0;
NSLog(@"%lf",pinchScale);
if (pinchScale < 1)
{
currentLabel.font = [UIFont fontWithName:currentLabel.font.fontName size:
(currentLabel.font.pointSize - pinchScale)];
recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale);
[currentLabel sizeToFit];
recognizer.scale=1;
}
else
{
currentLabel.font = [UIFont fontWithName:currentLabel.font.fontName size:(currentLabel.font.pointSize + pinchScale)];
recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale);
[currentLabel sizeToFit];
recognizer.scale=1;
}
//currentLabel.adjustsFontSizeToFitWidth = YES;
// [currentLabel sizeToFit];
NSLog(@"Font :%@",label.font);
}
gestureRecognizer.scale
start with 1.0 at the beginning of pinch (gestureRecognizer.state == .began), and gestureRecognizer.scale
in later state (.changed or .end) is always based on that, for example, if the view size is view_size
at the beginning of pinch (might not be the same with the original size orig_view_size
), gestureRecognizer.scale
always starts with 1.0, and if it becomes 2.0 later, it's size will be 2 * view_size
, so the scale always based on that when the pinch starts.
And we can get the scale at the beginning of pinch (gestureRecognizer.state == .began) lastScale = self.imageView.frame.width/self.imageView.bounds.size.width
, so the scale of the original image now should be lastScale * gestureRecognizer.scale
lastScale
: The scale of last round of Pinch, a round of Pinch is from state.start to state.end, and the scale is based on the original view size.
gestureRecognizer.scale
: current scale, based on the view size after last round of Pinch.
currentScale
: current scale, based on the orignial view size.
newScale
: new scale, based on the orignial view size. newScale = lastScale * gestureRecognizer.scale
, and you can limit the scale of the view by comparing the limitation with newScale
.
```
var lastScale:CGFloat = 1.0
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
var newScale = gestureRecognizer.scale
if gestureRecognizer.state == .began {
lastScale = self.imageView.frame.width/self.imageView.bounds.size.width
}
newScale = newScale * lastScale
if newScale < minScale {
newScale = minScale
} else if newScale > maxScale {
newScale = maxScale
}
let currentScale = self.imageView.frame.width/self.imageView.bounds.size.width
self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
print("last Scale: \(lastScale), current scale: \(currentScale), new scale: \(newScale), gestureRecognizer.scale: \(gestureRecognizer.scale)")
}
```
gestureRecognizer.scale
start with 1.0 on each Pinch notification, this require you reset gestureRecognizer.scale = 1
in the code in the end of each notification handler, so now gestureRecognizer.scale
is based on the view size of last Pinch notification, NOT based on the view size at the beginning of pinch. This is the most important difference with method 1. And since we don't rely on the scale of last round, we don't need lastScale
anymore.
currentScale
: current scale, based on the orignial view size.
gestureRecognizer.scale
: new scale, based on the view size of last Pinch (not the last round), the scale value based on the orignial view size will be currentScale * gestureRecognizer.scale
And we use transform.scaledBy
now, which use the scale based on view size of last Pinch (not the last round).
```
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
let currentScale = self.imageView.frame.width/self.imageView.bounds.size.width
var newScale = gestureRecognizer.scale
if currentScale * gestureRecognizer.scale < minScale {
newScale = minScale / currentScale
} else if currentScale * gestureRecognizer.scale > maxScale {
newScale = maxScale / currentScale
}
self.imageView.transform = self.imageView.transform.scaledBy(x: newScale, y: newScale)
print("current scale: \(currentScale), new scale: \(newScale)")
gestureRecognizer.scale = 1
}
```
There isn't a way to limit the scale on a UIPinchGestureRecognizer
. To limit the height in your code, you should be able to do something like this:
CGFloat scale = 1.0 - (lastScale - pinchscale);
CGRect bounds = [(UIPinchGestureRecognizer*)sender view].bounds;
scale = MIN(scale, maximumHeight / CGRectGetHeight(bounds));
scale = MAX(scale, minimumHeight / CGRectGetHeight(bounds));
To limit width, change 'Height' to 'Width' in the last two lines.