AppKit - Legal to Change a View's Frame in -viewDidLayout?

I have (had) a view controller that does a bit of manual layout in a -viewDidLayout override.

This was pretty easy to manage - however since introducing NSGlassEffectView into the view hierarchy I sometimes am getting hit with "Unable to simultaneously satisfy constraints" and Appkit would break a constraint to 'recover.' It appears translatesAutoresizingMaskIntoConstraints is creating some really weird fixed width and height constraints. Here I wasn't doing any autolayout - just add the glass view and set its frame in -viewDidLayout.

At runtime since I do manual layout in -viewDidLayout the frames are fixed and there is no real "error" in my app in practice though I wanted to get rid of the constraint breaking warning being logged because I know Autolayout can be aggressive about 'correctness' who knows if they decide to throw and not catch in the future.

In my perfect world I would probably just prefer a view.doesManualLayout = YES here - the subviews are big containers no labels so localization is not an issue for me. Rather than playing with autoresizing masks to get better translated constraints I decided to set translatesAutoresizingMaskIntoConstraints to NO and make the constraints myself. Now I get hit with the following exception:

"The window has been marked as needing another Layout Window pass, but it has already had more Layout Window passes than there are views in the window"

So this happens because the view which now has constraints -- I adjusted the frame of it one point in -viewDidLayout. My question is - is not legal to make changes in -viewDidLayout - which seems like the AppKit version of -viewDidLayoutSubviews.

In UIKit I always thought it was fine to make changes in -viewDidLayoutSubviews to frames - even if constraints were used - this is a place where you could override things in complex layouts that cannot be easily described in constraints. But in AppKit if you touch certain frames in -viewDidLayout it can now cause this exception (also related: https://developer.apple.com/forums/thread/806471)

I will change the constant of one of the constraints to account for the 1 point adjustment but my question still stands - is it not legal to touch frames in -viewDidLayout when autolayout constraints are used on that subview? It is (or at least was if I remember correctly) permitted to change the layout in -viewDidLayoutSubviews in UIKit but AppKit seems to be more aggressive in its checking for layout correctness).

What about calling -sizeToFit on a control in viewDidLayout or some method that has side effect of invalidating layout in a non obvious way, is doing things like this now 'dangerous?'

Shouldn't AppKit just block the layout from being invalidated from within -viewDidLayout - and leave whatever the layout is as is when viewDidLayout returns (thus making -viewDidLayout a useful place to override layout in the rare cases where you need a sledgehammer?)

Thanks for the post, it’s hard not seeing the code and how you're setting constrains in code. Would you be so kind to provide your code where you setting the constraints so developers here can see it?

What do you mean by legal? it is generally not a good idea (and will lead to crashes) to directly modify the frame or bounds of a view that is managed by Auto Layout in my opinion.

In my experience any method that directly or indirectly causes setNeedsLayout() or setNeedsDisplay() or changes the frame or bounds of a view that is managed by Auto Layout, may likely trigger the same infinite loop and exception.

I think AppKit's layout cycle (especially with Auto Layout) translatesAutoresizingMaskIntoConstraints creates "weird fixed width and height constraints" leading to "Unable to simultaneously satisfy constraints" warnings isn’t?

When you then add a view add an explicit Auto Layout constraints to a view hierarchy where translatesAutoresizingMaskIntoConstraints is true for some views, you often end up with conflicting constraints.

Your instinct to set translatesAutoresizingMaskIntoConstraints = false for views you intend to manage with Auto Layout should fix the issue. This tells AppKit, don't try to guess my layout; I'll provide all the constraints myself, if you modify the frame or bounds of a view that is managed by Auto Layout layout() or viewDidLayout, you implicitly invalidate its layout I think.

I would recommend to remove all constraints and let see how it looks first before making any decisions.

I believe there is a safety when AppKit detects this infinite loop and throws the "The window has been marked as needing another Layout Window pass..." exception to prevent your app from freezing.

Shouldn't AppKit just block the layout from being invalidated from within viewDidLayout and leave whatever the layout is?

When using Auto Layout, you should define your layout declaratively with constraints and avoid direct frame manipulation within layout() or viewDidLayout to prevent infinite loops and crashes.

I like SwiftUI better to avoid this layout issue I think...

Albert Pascual
  Worldwide Developer Relations.

Thanks a lot for responding.

Thanks for the post, it’s hard not seeing the code and how you're setting constrains in code. Would you be so kind to provide your code where you setting the constraints so developers here can see it? [..] it is generally not a good idea (and will lead to crashes) to directly modify the frame or bounds of a view that is managed by Auto Layout in my opinion.

Initially I did not involve Autolayout at all explicitly. What I had was a small container view controller with something like this (no explicit constraints):

@implementation SmallInfoWrapperViewController

-(void)viewDidLayout
{
  [super viewDidLayout];
  NSRect bounds = self.view.bounds;
  CGFloat lineHeight = 1.0;
  self.separator.frame = NSMakeRect(0.0,bounds.size.height-lineHeight,bounds.size.width,lineHeight);

  BOOL separatorVisible = !self.separator.isHidden;
  
CGFloat wrappedViewHeight = bounds.size.height;
if (separatorVisible) { wrappedViewHeight -= lineHeight; }
  self.wrappedView.frame = NSMakeRect(0.0,0.0,bounds.size.width, wrappedViewHeight);
}
@end

That was fine when wrappedView was an NSScrollView (which contains an NSOutlineView).

The NSScrollView/OutlineView/Clipview triplet comes out of a xib which is many years old that also has no explicit Autolayout constraints. All worked fine. Once wrappedView became an NSGlassEffectView (which now contains the scroll view) that is when Autolayout said it needed to break constraints the system was creating from autoresizing masks and it listed weird fixed width and height constraints.

I find Autolayout useful but avoided it in certain situations where it seems unnecessary it could overcomplicate and the task was so easy (like this one) it felt like cutting a stick of butter with a chainsaw. But the system seems to be trying to make Autolayout somewhat of a requirement so rather than try to fight it I just gave in.

self.wrappedView.translatesAutoresizingMaskIntoConstraints = NO;
        [NSLayoutConstraint activateConstraints:@[leading,
                                                  trailing,
                                                  bottom,
                                                  top]];

But I initially forgot to remove self.wrappedView.frame from -viewDidLayout which is when I hit the exception. Fix is to instead modify the constant of the top constraint when separator.isHidden changes instead. So I already knew what caused the issue and how to fix it but the exception: "The window has been marked as needing another Layout Window pass, but it has already had more Layout Window passes than there are views in the window" did make me curious enough to ask this question because it doesn't seem so uncommon/unreasonable to make adjustments in -layout and -viewDidLayout. Something like:

-(void)layout
{
   [super layout];
  [self.dateLabel sizeToFit];
   if (![self doesDateLabelFitAtNaturalWidth])
   {
        [self useShorterDateStyleLayout];
   }
} 

Of course the above is just "concept code" but shrinking a control based on available space in certain situations is more straightforward and easier to maintain in manual layout than with Autolayout. Most of the time Autolayout is preferred, I agree.

In any case it doesn't feel appropriate for the system to generate width and height constraints from autoresizing masks ever (perhaps I have a naive point of view but that looks like it could cause some real trouble to me). IMO it makes sense to generate leading, trailing, top and bottom constraints but width/height I'm not sure about that. There was like some weird 250 point min width constraint or something and I have no idea how that possibly could have been calculated from autoresizing masks. In another area I had to fix I also ran into a min width constraint conflict when I downsized a window width because the generated constraint from an autoresizing mask somehow created a weird min width 50 constraint that was too big for the really small window size.

Overall it now seems somewhat dicey to set frames in -viewDidLayout / layout overrides because even if you aren't using constraints explicitly it is difficult to determine what the system is generating under the covers.

What do you mean by legal?

What I mean by legal is safe to modify and supported. In my opinion I think there should be a point (like UIKit -viewDidLayoutSubviews) where layout can be safely modified with or without explicit Autolayout constraints to give developers maximum control (even though rarely needed). I don't really understand why the system doesn't permit a more hands off approach like this:

self.lockLayoutInvalidation = YES;
[self doAutolayoutOnSubTree];
[self callViewDidLayout];
self.lockLayoutInvalidation = NO;

Whatever the layout is after viewDidLayout - it is what it is. Any overlapping views or mistakes or whatever leave it on the developer to find and fix but crashing, throwing, etc. seems a bit much.

AppKit - Legal to Change a View's Frame in -viewDidLayout?
 
 
Q