Search code examples
cocoaautolayoutappkitnsbuttonnssegmentedcontrol

Autolayout: conditional content compression resistance priority using NSUserInterfaceCompression


I've created a custom segment control (Cocoa / macOS) by subclassing NSView (does not use any existing controls / buttons; it's an entirely custom view with a complex set of internal constraints) that has two modes:

  1. Displays all segments horizontally by default: [ segment 1 ] [ segment 2 ] [ segment 3 ]
  2. Displays a single segment as a drop down when all segments cannot fit in the window / current set of constraints (influenced by surrounding controls and their constraints): [ segment 1 🔽 ]

This works just fine, and I'm able to switch between / animate the two modes at runtime. However what I want to ultimately achieve is automatic expansion / compression based on the current window size (or switch between the two when the user is resizing the window). I want this control to be reusable without the window / view controller managing the switch, and trying to avoid switching between constraints based on 'rough' estimates from inside of a superview's layout call (which feels like a hack).

It seems NSSegmentControl, NSButton etc implement NSUserInterfaceCompression which should do what I am trying to achieve, however none of the methods in that protocol get called at any time during initial layout / intrinsic content size refresh / window resize etc. I also find the documentation lacking; the only useful information I found was inside the NSSegmentControl header files. The protocol seems to be exactly what I need - for the system to call the appropriate methods to determine a minimum / ideal size and ask the control to resize itself when space is at a premium.

For what it's worth, I've tried subclassing NSButton too (for various reasons, I need to stick to subclassing NSView) - however that did not trigger any of these methods either (i.e. from NSUserInterfaceCompression).

Any idea what I'm missing?


Solution

  • It seems NSUserInterfaceCompression is a dead end. For now I've reported this as feedback / bug regarding the inadequate documentation (FB9062854).

    The way I ended up solving this is by:

    1. Setting the following content compression on the custom control:
        let priorityToResistCompression = contentCompressionResistancePriority(for: .horizontal)
        setContentCompressionResistancePriority(priorityToResistCompression, for: .horizontal)
    
    1. The last segment (inner NSView subviews) within the control has a trailing anchor set with priority defaultLow to allow it to break so that the control can continue to stretch

    2. Override setFrameSize and determine the best mode to display (compressed, single segment as a drop down, or all the segments if they can fit horizontally). Then call invalidateIntrinsicContentSize() for the content size to be recomputed.

    3. Using the mode determined in the previous step, override intrinsicContentSize and offer the correct size (the minimum compressed version or the one where all segments can fit).

    This way the control wraps all of this functionality into a single NSView subclass and relieves any superview / window hosting this control of setting the correct size as the window is resized.