Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS: CGAffineTransformScale moves my object

Building on a question I had earlier.

Simple button trying to transform a label. I want it to shrink by 0.5, which works but for some reason it also moves the object as it does it. The label jumps up and to the left, then transforms.

- (IBAction)btnTest:(id)sender
{

    [UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        lblTest.transform = CGAffineTransformScale(lblTest.transform, 0.5f,0.5f);
    }completion:^(BOOL finished) {
        if(finished){
            NSLog(@"DONE");
        }
    }];
}
like image 743
JoshDG Avatar asked Apr 29 '13 23:04

JoshDG


People also ask

What is CGAffineTransform identity?

The CGAffineTransform type provides functions for creating, concatenating, and applying affine transformations. Affine transforms are represented by a 3 by 3 matrix: Because the third column is always (0,0,1) , the CGAffineTransform data structure contains values for only the first two columns.


1 Answers

I'm presuming from the question that you're using auto layout: In auto layout, if you have a leading and/or top constraint, after you scale with CGAffineTransformMakeScale, the leading/top constraint will be reapplied and your control will move on you in order to ensure that the constraint is still satisfied.

You can either turn off auto layout (which is the easy answer) or you can:

  • wait until viewDidAppear (because constraints defined in IB be applied, and the control will be placed where we want it and its center property will be reliable);

  • now that we have the center of the control in question, replace the leading and top constraints with NSLayoutAttributeCenterX and NSLayoutAttributeCenterY constraints, using the values for center property to set the constant for the NSLayoutConstraint as as follows.

Thus:

// don't try to do this in `viewDidLoad`; do it in `viewDidAppear`, where the constraints
// have already been set

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self replaceLeadingAndTopWithCenterConstraints:self.imageView];
}

// Because our gesture recognizer scales the UIView, it's quite important to make
// sure that we don't have the customary top and leading constraints, but rather
// have constraints to the center of the view. Thus, this looks for leading constraint
// and if found, removes it, replacing it with a centerX constraint. Likewise if it
// finds a top constraint, it replaces it with a centerY constraint.
//
// Having done that, we can now do `CGAffineTransformMakeScale`, and it will keep the
// view centered when that happens, avoiding weird UX if we don't go through this
// process.

- (void)replaceLeadingAndTopWithCenterConstraints:(UIView *)subview
{
    CGPoint center = subview.center;

    NSLayoutConstraint *leadingConstraint = [self findConstraintOnItem:subview
                                                             attribute:NSLayoutAttributeLeading];
    if (leadingConstraint)
    {
        NSLog(@"Found leading constraint");

        [subview.superview removeConstraint:leadingConstraint];

        [subview.superview addConstraint:[NSLayoutConstraint constraintWithItem:subview
                                                                      attribute:NSLayoutAttributeCenterX
                                                                      relatedBy:NSLayoutRelationEqual
                                                                         toItem:subview.superview
                                                                      attribute:NSLayoutAttributeTop
                                                                     multiplier:1.0
                                                                       constant:center.x]];
    }

    NSLayoutConstraint *topConstraint = [self findConstraintOnItem:subview
                                                         attribute:NSLayoutAttributeTop];

    if (topConstraint)
    {
        NSLog(@"Found top constraint");

        [subview.superview removeConstraint:topConstraint];

        [subview.superview addConstraint:[NSLayoutConstraint constraintWithItem:subview
                                                                      attribute:NSLayoutAttributeCenterY
                                                                      relatedBy:NSLayoutRelationEqual
                                                                         toItem:subview.superview
                                                                      attribute:NSLayoutAttributeLeft
                                                                     multiplier:1.0
                                                                       constant:center.y]];
    }
}

- (NSLayoutConstraint *)findConstraintOnItem:(UIView *)item attribute:(NSLayoutAttribute)attribute
{
    // since we're looking for the item's constraints to the superview, let's
    // iterate through the superview's constraints

    for (NSLayoutConstraint *constraint in item.superview.constraints)
    {
        // I believe that the constraints to a superview generally have the
        // `firstItem` equal to the subview, so we'll try that first.

        if (constraint.firstItem == item && constraint.firstAttribute == attribute)
            return constraint;

        // While it always appears that the constraint to a superview uses the
        // subview as the `firstItem`, theoretically it's possible that the two
        // could be flipped around, so I'll check for that, too:

        if (constraint.secondItem == item && constraint.secondAttribute == attribute)
            return constraint;
    }

    return nil;
}

The particulars of your implementation may vary depending upon how you've defined the constraints of the control you want to scale (in my case, leading and top were based upon the superview, which made it easier), but hopefully it illustrates the solution, to remove those constraints and add new ones based upon the center.

You could, if you didn't want to iterate through looking for the constraint in question, like I do above, define an IBOutlet for the top and leading constraints instead, which greatly simplifies the process. This sample code was taken from a project where, for a variety of reasons, I couldn't use the IBOutlet for the NSLayoutConstraint references. But using the IBOutlet references for the constraints is definitely an easier way to go (if you stick with auto layout).

For example, if you go to Interface Builder, you can highlight the constraint in question and control-drag to the assistant editor to make your IBOutlet:

make constraint IBOutlet

If you do that, rather than iterating through all of the constraints, you now can just say, for example:

if (self.imageViewVerticalConstraint)
{
    [self.view removeConstraint:self.imageViewVerticalConstraint];

    // create the new constraint here, like shown above
}

Frankly, I wish Interface Builder had the ability to define constraints like these right out of the box (i.e. rather than a "leading of control to left of superview" constraint, a "center of control to left of superview" constraint), but I don't think it can be done in IB, so I'm altering my constraints programmatically. But by going through this process, I can now scale the control and not have it move around on me because of constraints.


As 0x7fffffff noted, if you apply a CATransform3DMakeScale to the layer, it will not automatically apply the constraints, so you won't see it move like if you apply CGAffineTransformMakeScale to the view. But if you do anything to reapply constraints (setNeedsLayout or do any changes to any UIView objects can cause the constraints to be reapplied), the view will move on you. So you might be able to "sneak it in" if you restore the layer's transform back to identity before constraints are reapplied, but it's probably safest to turn off autolayout or just fix the constraints.

like image 68
Rob Avatar answered Sep 18 '22 12:09

Rob