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
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
:
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.