Uiview/Calayer: Transform Triggers Layoutsubviews in Superview

UIView/CALayer: Transform triggers layoutSubviews in superview

Apple answered me via TSI (I personally think this is rubbish):

Part 1

why am I seeing this behavior?
is this inconsistency or am I misunderstanding some core concepts?

A view will be flagged for layout whenever the system feels something has changed that requires the view to re-calculate the frames of its subviews. This may occur more often than you'd expect and exactly when the system chooses to flag a view as requiring layout is an implementation detail.

why does it cascade upwards the view hierarchy?

Generally, changing a geometric property of a view (or layer) will trigger a cascade of layout invalidations up the view hierarchy because parent views may have Auto Layout constraints involving the modified child. Note that Auto Layout is active in some form regardless of whether you have explicitly enabled it.

how can I avoid superview to layoutSubviews every time I'm changing transform?

There is no way to bypass this behavior. It's part of UIKit's internal bookkeeping that is required to keep the view hierarchy consistent.

Part 2

Hi Håvard,

But if this is true I really can not understand why this does
not apply to 'layer.anchorPoint' and 'center'/'layer.position'.

It may be that we are more conservative in this case. Parent views don't need to care about the position of their children except when Auto Layout gets involved. And if Auto layout were involved, you'd need to modify the constraints directly to produce a lasting adjustment in the position anyway.

This transform triggers layoutSubviews which again cascades upwards.

It is my understanding that a change to the transform only invalidates the layout of the view's immediate parent (unless you have setup constraints to the changed view, then it becomes more complicated). Also, layout invalidations are batched so your parent's layout subview's method should only be called once per transaction (frame). Still, I can understand that this may cause performance issues if your layout logic is complicated.

Any ideas?

Wrap your cell contents in an intermediate view. When you modify the transform of this intermediate view, only the cell's layout should be invalidated, so your expensive layout method won't be called.

If that doesn't work, create some mechanism to signal to your expensive layout method when it actually does (or does not) have to do work. This could be a property you set when the only changes you make are to the transforms.

Subviews layer transform and layoutSubviews

I discovered that is possible to solve this problem in many ways:

1) Temporary save the transform, change the frame and than reapply the transform:

[super layoutSubviews];

//backup the transform value before the layout
CATransform3D transform = self.internalView.layer.transform;

//Set identity and update all the necessary frames
self.internalView.layer.transform = CATransform3DIdentity;
self.internalView.frame = self.bounds;

//set back the transform
self.internalView.layer.transform = transform;

2) Change the bounds and the center of the view without touch the frame. Bounds and center seems to not affect the transform. Make sense because the frame is calculated internally reading layer's bounds, position, transform, anchor point.

self.internalView.bounds = self.bounds;
self.internalView.center = CGPointMake(floorf(CGRectGetWidth(self.bounds)/2.0f), floorf(CGRectGetHeight(self.bounds)/2.0f));

3) Apple in a TSI (reported here UIView/CALayer: Transform triggers layoutSubviews in superview) suggests to

"Wrap your cell contents in an intermediate view. When you modify the
transform of this intermediate view, only the cell's layout should be
invalidated, so your expensive layout method won't be called.

If that doesn't work, create some mechanism to signal to your
expensive layout method when it actually does (or does not) have to do
work. This could be a property you set when the only changes you make
are to the transforms."

4) I also experimented turning off completely autolayout for the subviews of my custom view. You can read this really good article from obj.io where is explained that if you remove the call to [super layoutSubviews] from the layoutSubviews implementation you are opting out autolayout for your custom view and all its subviews.

The tread off is that you must be aware that in the case you add as subview a view with constrains the autolayout system will rise an exception like this:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still
required after executing -layoutSubviews. MyView's implementation of
-layoutSubviews needs to call super.'

The solution 1 and 2 are good if you have a simple layout logic. If your layout logic is expansive you can implement 3 or 4 and referee to this other question UIView/CALayer: Transform triggers layoutSubviews in superview

How do I adjust the anchor point of a CALayer, when Auto Layout is being used?

[EDIT: Warning: The entire ensuing discussion will be possibly outmoded or at least heavily mitigated by iOS 8, which may no longer make the mistake of triggering layout at the time that a view transform is applied.]

Autolayout vs. View Transforms

Autolayout does not play at all well with view transforms. The reason, as far as I can discern, is that you're not supposed to mess with the frame of a view that has a transform (other than the default identity transform) - but that is exactly what autolayout does. The way autolayout works is that in layoutSubviews the runtime comes dashing through all the constraints and setting the frames of all the views accordingly.

In other words, the constraints are not magic; they are just a to-do list. layoutSubviews is where the to-do list gets done. And it does it by setting frames.

I can't help regarding this as a bug. If I apply this transform to a view:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

I expect to see the view appear with its center in the same place as before and at half the size. But depending on its constraints, that may not be what I see at all.

[Actually, there's a second surprise here: applying a transform to a view triggers layout immediately. This seems to me be another bug. Or perhaps it's the heart of the first bug. What I would expect is to be able to get away with a transform at least until layout time, e.g. the device is rotated - just as I can get away with a frame animation until layout time. But in fact layout time is immediate, which seems just wrong.]

Solution 1: No Constraints

One current solution is, if I'm going to apply a semipermanent transform to a view (and not merely waggle it temporarily somehow), to remove all constraints affecting it. Unfortunately this typically causes the view to vanish from the screen, since autolayout still takes place, and now there are no constraints to tell us where to put the view. So in addition to removing the constraints, I set the view's translatesAutoresizingMaskIntoConstraints to YES. The view now works in the old way, effectively unaffected by autolayout. (It is affected by autolayout, obviously, but the implicit autoresizing mask constraints cause its behavior to be just like it was before autolayout.)

Solution 2: Use Only Appropriate Constraints

If that seems a bit drastic, another solution is to set the constraints to work correctly with an intended transform. If a view is sized purely by its internal fixed width and height, and positioned purely by its center, for example, my scale transform will work as I expect. In this code, I remove the existing constraints on a subview (otherView) and replace them with those four constraints, giving it a fixed width and height and pinning it purely by its center. After that, my scale transform works:

NSMutableArray* cons = [NSMutableArray array];
for (NSLayoutConstraint* con in self.view.constraints)
if (con.firstItem == self.otherView || con.secondItem == self.otherView)
[cons addObject:con];

[self.view removeConstraints:cons];
[self.otherView removeConstraints:self.otherView.constraints];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterX relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.otherView.center.x]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:self.otherView.center.y]];
[self.otherView addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.width]];
[self.otherView addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.height]];

The upshot is that if you have no constraints that affect a view's frame, autolayout won't touch the view's frame - which is just what you're after when a transform is involved.

Solution 3: Use a Subview

The problem with both the above solutions is that we lose the benefits of constraints to position our view. So here's a solution that solves that. Start with an invisible view whose job is solely to act as a host, and use constraints to position it. Inside that, put the real view as a subview. Use constraints to position the subview within the host view, but limit those constraints to constraints that won't fight back when we apply a transform.

Here's an illustration:

Sample Image

The white view is host view; you are supposed to pretend that it is transparent and hence invisible. The red view is its subview, positioned by pinning its center to the host view's center. Now we can scale and rotate the red view around its center without any problem, and indeed the illustration shows that we have done so:

self.otherView.transform = CGAffineTransformScale(self.otherView.transform, 0.5, 0.5);
self.otherView.transform = CGAffineTransformRotate(self.otherView.transform, M_PI/8.0);

And meanwhile the constraints on the host view keep it in the right place as we rotate the device.

Solution 4: Use Layer Transforms Instead

Instead of view transforms, use layer transforms, which do not trigger layout and thus do not cause immediate conflict with constraints.

For example, this simple "throb" view animation may well break under autolayout:

[UIView animateWithDuration:0.3 delay:0
options:UIViewAnimationOptionAutoreverse
animations:^{
v.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
v.transform = CGAffineTransformIdentity;
}];

Even though in the end there was no change in the view's size, merely setting its transform causes layout to happen, and constraints can make the view jump around. (Does this feel like a bug or what?) But if we do the same thing with Core Animation (using a CABasicAnimation and applying the animation to the view's layer), layout doesn't happen, and it works fine:

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.autoreverses = YES;
ba.duration = 0.3;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1)];
[v.layer addAnimation:ba forKey:nil];

Loading UIView transform & center from settings gives different position

Thanks to all who contributed answers! The sum of them all led me to the following:

The trouble seems to have been that the bounds CGRect was being reset after loading the transform from preferences at startup, but not when updating the preferences while modifying in real time.

I think there are two solutions. One would be to first load the preferences from layoutSubviews instead of from viewDidLoad. Nothing seems to happen to bounds after layoutSubviews is called.

For other reasons in my app, however, it's more convenient to load the preferences from the view controller's viewDidLoad. So the solution I'm using is this:

// UserTransformableView.h
@interface UserTransformableView : UIView {
CGRect defaultBounds;
}

// UserTransformableView.m
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if( self ) {
defaultBounds = self.bounds;
}
return self;
}

- (void)layoutSubviews {
self.bounds = defaultBounds;
}


Related Topics



Leave a reply



Submit