2
votes

I'm trying to rotate, pan and zoom an UIView using UIGestureRecognizers. The recognisers are added to the superview, while the rotation,zoom etc is applied to a subview (_baseImage) this is to enable me to overlay other things on top of the subview in future while still receiving the gesture events.

The idea is that the UIView should scale/rotate around an "anchor" point on the subview underneath the two touch points as this seems the most natural. What I'm having problems with is the position of the subview after setting the anchorPoint, and also that the scale and rotation doesn't seem to use the set anchorPoint. My lack of understanding of overlapping co-ordinate systems/CGAffine transforms may be getting me into trouble. Code is pulled from various examples.

Here's my code as it stands at the moment:

-(void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view
{
    CGPoint oldOrigin = view.frame.origin;
    view.layer.anchorPoint = anchorPoint;
    CGPoint newOrigin = view.frame.origin;

    CGPoint transition;
    transition.x = newOrigin.x - oldOrigin.x;
    transition.y = newOrigin.y - oldOrigin.y;

    view.center = CGPointMake (view.center.x - transition.x, view.center.y - transition.y);

}
- (void) updateTransformWithOffset: (CGPoint) translation
{
    // Create a blended transform representing translation,
    // rotation, and scaling
    _baseImage.transform = CGAffineTransformMakeTranslation(translation.x + tx, translation.y + ty);
    _baseImage.transform = CGAffineTransformRotate(_baseImage.transform, theta);
    _baseImage.transform = CGAffineTransformScale(_baseImage.transform, scale, scale);
}
- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)uigr {
    if (uigr.state == UIGestureRecognizerStateBegan) {
        UIView *piece = self.view;
        CGPoint locationInView = [uigr locationInView:_baseImage];
        myFrame = _baseImage.frame;
        CGPoint newAnchor = CGPointMake( (locationInView.x / piece.bounds.size.width), (locationInView.y / piece.bounds.size.height ));
       [self setAnchorPoint:newAnchor forView:_baseImage];
    }
}
- (void) handlePinch: (UIPinchGestureRecognizer *) uigr
{
    [self adjustAnchorPointForGestureRecognizer:uigr];
    if (uigr.state == UIGestureRecognizerStateBegan) {
        initScale = scale;
    }
    scale = initScale*uigr.scale;
    [self updateTransformWithOffset:CGPointZero];
}
2
Yes, you will see in my code that I'm using anchorPoint… problem is I can't seem to get it to work as expected.user3306929
I see that now. I just saw a large amount of code dealing with transforms.David Rönnqvist
Your questions is not very precise and your code is quite complex. I suggest that you try and break it down into pieces that work well and pieces that have problems. Then remove every thing else until you have the minimal amount of code that reproduces your problem.David Rönnqvist
I've tried to simplify and removed everything except for the scale element.user3306929

2 Answers

8
votes

I've found a solution to my specific problem with the anchorPoint. I was trying to apply transforms to a UIView added in Interface Builder which caused problems with the anchor point and setting new centre point, removing then re-adding the subview seemed to fix it. If anyone has similar problems here is the final code I used on my view controller, it does scale,rotate and pan on a UIView using the touch position as centre for rotate and scale:

#import "ViewController.h"

@interface ViewController (){
    CGFloat tx; // x translation
    CGFloat ty; // y translation
    CGFloat scale; // zoom scale
    CGFloat theta; // rotation angle
    CGFloat initScale ;
    CGFloat initTheta ;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    UIRotationGestureRecognizer *rotationGesture = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotation:)];
    [rotationGesture setDelegate:self];
    [self.view addGestureRecognizer:rotationGesture];
    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
    [pinchGesture setDelegate:self];
    [self.view addGestureRecognizer:pinchGesture];
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [panGesture setDelegate:self];
    [panGesture setMinimumNumberOfTouches:1];
    [panGesture setMaximumNumberOfTouches:1];
    [self.view addGestureRecognizer:panGesture];
    _baseImage.transform = CGAffineTransformIdentity;
    tx = 0.0f; ty = 0.0f; scale = 1.0f; theta = 0.0f;
    scale = 1.0;
    //removing and adding back to the view seems to fix problems with anchor point I was having, I suspect because of IB layout/scaling and constraints etc
    UIView *mySuperView =_baseImage.superview;
    [_baseImage removeFromSuperview];
    [mySuperView addSubview:_baseImage];
}
-(void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)myview
{
    CGPoint oldOrigin = myview.frame.origin;
    myview.layer.anchorPoint = anchorPoint;
    CGPoint newOrigin = myview.frame.origin;
    CGPoint transition;
    transition.x = (newOrigin.x - oldOrigin.x);
    transition.y = (newOrigin.y - oldOrigin.y);
    CGPoint myNewCenter = CGPointMake (myview.center.x - transition.x, myview.center.y - transition.y);
    myview.center =  myNewCenter;
}
- (void) updateTransformWithOffset: (CGPoint) translation
{
    // Create a blended transform representing translation,
    // rotation, and scaling
    _baseImage.transform = CGAffineTransformMakeTranslation(translation.x + tx, translation.y + ty);
    _baseImage.transform = CGAffineTransformRotate(_baseImage.transform, theta);
    _baseImage.transform = CGAffineTransformScale(_baseImage.transform, scale, scale);
}
- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)uigr {
    if (uigr.state == UIGestureRecognizerStateBegan) {
        tx =_baseImage.transform.tx;
        ty =_baseImage.transform.ty;
        CGPoint locationInView = [uigr locationInView:_baseImage];
        CGPoint newAnchor = CGPointMake( (locationInView.x / _baseImage.bounds.size.width), (locationInView.y / _baseImage.bounds.size.height ));
        [self setAnchorPoint:newAnchor forView:_baseImage];
    }
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    // if the gesture recognizers are on different views, don't allow simultaneous recognition
    if (gestureRecognizer.view != otherGestureRecognizer.view)
        return NO;

    if (![gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && ![otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        return YES;
    }
    return NO;
}
- (void) handleRotation: (UIRotationGestureRecognizer *) uigr
{
    if (uigr.state == UIGestureRecognizerStateBegan) {
        initTheta = theta;
    }
    theta = initTheta+uigr.rotation;
    [self adjustAnchorPointForGestureRecognizer:uigr];
    [self updateTransformWithOffset:CGPointZero];
}
- (void) handlePinch: (UIPinchGestureRecognizer *) uigr
{
    if (uigr.state == UIGestureRecognizerStateBegan) {
        initScale = scale;
    }
    scale = initScale*uigr.scale;
    [self adjustAnchorPointForGestureRecognizer:uigr];
    [self updateTransformWithOffset:CGPointZero];

}
- (void) handlePan: (UIPanGestureRecognizer *) uigr
{
    CGPoint translation = [uigr translationInView:_baseImage.superview];
    [self adjustAnchorPointForGestureRecognizer:uigr];
    [self updateTransformWithOffset:translation];
}
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end
2
votes

The OP's original solution translated to Swift 2.0. I left out the workaround since it doesn't seem to be an issue anymore.

class ViewController: UIViewController {

    var tx:CGFloat = 0.0 // x translation
    var ty:CGFloat = 0.0 // y translation
    var scale:CGFloat = 1.0 // zoom scale
    var theta:CGFloat = 0.0 // rotation angle
    var initScale:CGFloat = 1.0
    var initTheta:CGFloat = 0.0
    var transformedView: UIView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        transformedView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        transformedView.backgroundColor = UIColor.blueColor()
        view.addSubview(transformedView)

        let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(OHPlastyViewController.handleRotation(_:)))
        rotationGesture.delegate = self
        view.addGestureRecognizer(rotationGesture)


        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(OHPlastyViewController.handlePinch(_:)))
        pinchGesture.delegate = self
        view.addGestureRecognizer(pinchGesture)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(OHPlastyViewController.handlePan(_:)))
        panGesture.delegate = self
        panGesture.minimumNumberOfTouches = 1
        panGesture.maximumNumberOfTouches = 1
        view.addGestureRecognizer(panGesture)

        // The workaround wasn't translated to Swift because it doesn't seem to be needed anymore
        /*
         _baseImage.transform = CGAffineTransformIdentity;
         //removing and adding back to the view seems to fix problems with anchor point I was having, I suspect because of IB layout/scaling and constraints etc
         UIView *mySuperView =_baseImage.superview;
         [_baseImage removeFromSuperview];
         [mySuperView addSubview:_baseImage];*/
    }

    func setAnchorPoint(anchorPoint: CGPoint, forView myView: UIView) {
        let oldOrigin: CGPoint = myView.frame.origin
        myView.layer.anchorPoint = anchorPoint
        let newOrigin = myView.frame.origin
        let transition = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)
        let myNewCenter = CGPoint(x: myView.center.x - transition.x, y: myView.center.y - transition.y)
        myView.center = myNewCenter
    }

    func updateTransformWithOffset(translation: CGPoint) {
        transformedView.transform = CGAffineTransformMakeTranslation(translation.x + tx, translation.y + ty)
        transformedView.transform = CGAffineTransformRotate(transformedView.transform, theta)
        transformedView.transform = CGAffineTransformScale(transformedView.transform, scale, scale)
    }

    func adjustAnchorPointForGestureRecognizer(recognizer: UIGestureRecognizer) {
        if (recognizer.state == .Began) {
            tx = transformedView.transform.tx
            ty = transformedView.transform.ty
            let locationInView = recognizer.locationInView(transformedView)
            let newAnchor = CGPoint(x: (locationInView.x / transformedView.bounds.size.width), y: (locationInView.y / transformedView.bounds.size.height))
            setAnchorPoint(newAnchor, forView: transformedView)
        }
    }

    func handleRotation(recognizer: UIRotationGestureRecognizer) {
        if recognizer.state == .Began {
            initTheta = theta
        }
        theta = initTheta + recognizer.rotation
        adjustAnchorPointForGestureRecognizer(recognizer)
        updateTransformWithOffset(CGPointZero)
    }

    func handlePinch(recognizer: UIPinchGestureRecognizer) {
        if recognizer.state == .Began {
            initScale = scale
        }
        scale = initScale * recognizer.scale
        adjustAnchorPointForGestureRecognizer(recognizer)
        updateTransformWithOffset(CGPointZero)
    }

    func handlePan(recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translationInView(transformedView.superview)
        adjustAnchorPointForGestureRecognizer(recognizer)
        updateTransformWithOffset(translation)
    }
}

extension ViewController: UIGestureRecognizerDelegate {

    func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer.view != otherGestureRecognizer.view {
            return false
        }

        if !gestureRecognizer.isKindOfClass(UITapGestureRecognizer.self) && !otherGestureRecognizer.isKindOfClass(UITapGestureRecognizer.self) {
            return true
        }
        return false
    }
}