2
votes

I wish to embed an NSImageView in an NSScrollView such that when the NSScrollView is resized, its document view (ie, the NSImageView) is resized horizontally (up or down) to match the width of the NSScrollView, while its contents are scaled to preserve the image's aspect ratio. Consequently the NSImageView may or may not be clipped by the NSScrollView and vertical scrolling may be possible.

This may make things clearer: If what I wanted had a menu option in my program under its "View" menu, it would be titled "Zoom to fit horizontally."

Here's what I've done:

  1. Create an NSImageView in the layout editor.
  2. Use "Embed in..." to embed it in an NSScrollView.
  3. Enable "Proportionally Up or Down" for the "Scaling" option in the attributes editor for the NSImageView.
  4. Add constraints to pin the margins of the NSScrollView to its container.
  5. Add constraints to pin the left and right sides of the NSImageView to its container.
  6. Reduce the horizontal content compression resistance priority of the NSImageView to 251.

The NSScrollView resizes as expected. The NSImageView's left and right (or "leading" and "trailing," in auto-layout constraint parlance) margins are constrained to match the NSScrollView. The image in the NSImageView will be scaled proportionally down so that it fits in the NSScrollView. Vertical scrolling is possible when the NSScrollView is not tall enough to display the whole image.

There are two problems.

First, the NSImageView will increase its vertical size as necessary to maintain the aspect ratio, but when the NSScrollView shrinks (ie, because the containing window is made smaller) the NSImageView never reduces its vertical size. Consequently, there may be a bunch of empty space above and below the image.

My woefully under-informed guess as to what's going here is this: When the window grows, the layout engine tells the NSImageView that it's going to be larger horizontally and the NSImageView sees that it must resize its content (as it has been instructed to do--step 3) and says that it would like to be larger vertically. When the window is made smaller, the NSImageView learns that it will shrink horizontally, but it has no problem containing empty space, so it does not ask to be smaller vertically.

I've tried playing with vertical content compression and hugging priorities, as well as setting a low-priority height constraint of =1 (and >=1) on the NSImageView in hopes that this would gently encourage it to be as small as it can without contradicting some other constraint, but this has done is taught me that I have a very poor understanding of auto-layout.

Do I need to programmatically reset the vertical size of the NSImageView when it learns that its horizontal size is being changed?

The second problem is that the image does not grow horizontally beyond its natural size. Since the NSImageView is constrained on its leading/trailing edges to match its container and since it has been told to scale its contents up or down, I can't even begin to guess why this is happening.

1
I've determined that while the NSImageView is resized, its width changes according to the constraints placed on its leading and trailing edges, but its height never changes. Its height is set to the height of the unscaled image. It would seem that the content height is not updated when the image is scaled.Jamborino

1 Answers

2
votes

Programmatically applying these constraints when the image in the image view is updated seems to do the trick:

float aspect = image.size.height / image.size.width;
[imageView.heightAnchor constraintEqualToAnchor:imageView.widthAnchor
                                     multiplier:aspect].active = YES;
[scrollView.superview.heightAnchor constraintLessThanOrEqualToAnchor:imageView.heightAnchor].active = YES;

This is in addition to the following constraints created in the interface builder:

  1. Image View.top = Superview.top
  2. Image View.leading = Superview.leading
  3. Image View.trailing = Superview.trailing
  4. Superview.bottom = Scroll View.bottom - 1
  5. Superview.top = Scroll View.top - 1
  6. Superview.leading = Scroll View.leading - 1
  7. Superview.trailing = Scroll View.trailing - 1