5

First of all, I want to say that I've looked at a good number of other resources trying to accomplish this, but nothing I've looked at seems to help.

For instance: Automatically grow document view of NSScrollView using auto layout?

I have an NSScrollView that I created in Interface Builder and set constraints on to fill the window it's in: Top, Left, Right, and Bottom of the scrollview are set equal to the Top, Left, Right, and Bottom of the superview (which is the xib view used by the viewcontroller in Xamarin).

My intended document view is a simple NSView that is being dynamically filled with NSImageViews within my viewcontroller's ViewDidLoad:

EventListScrollView.TranslatesAutoresizingMaskIntoConstraints = false;

var documentView = new NSView(EventListScrollView.Bounds);
//documentView.AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.HeightSizable;
//documentView.TranslatesAutoresizingMaskIntoConstraints = false;

NSView lastHeader = null;
foreach(var project in _projects)
{
    var imageView = new NSImageView();
    imageView.TranslatesAutoresizingMaskIntoConstraints = false;
    imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Horizontal);
    imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Vertical);
    imageView.ImageScaling = NSImageScale.ProportionallyUpOrDown;

    var xPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Left, 1, 0);
    NSLayoutConstraint yPosConstraint = null;
    if(lastHeader!=null)
    {
        yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
    }
    else
    {
        yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Top, 1, 0);
    }

    var widthConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Width, 1, 0);
    var heightConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, imageView, NSLayoutAttribute.Width, (nfloat)0.325, 0);

    documentView.AddSubview(imageView);
    documentView.AddConstraints(new[] { xPosConstraint, yPosConstraint, widthConstraint, heightConstraint });

    lastHeader = imageView;
    ImageService.Instance.LoadUrl($"https:{project.ProjectLogo}").Into(imageView);
}

EventListScrollView.DocumentView = documentView;

Since I don't necessarily know how many projects are in _projects and I don't know the height of the image that will be loading into each imageView, though I do know the intended ratio, I don't know the height of my documentView until runtime.

I want the documentView's width to be resized to be the same as the NSClipView every time the enclosing window the scrollview is in (and therefore also the scrollview) is resized. As for the height of the documentView, I want it to be determined by the size of the imageViews I'm populating in the documentView, which is why the height constraint is relative to the width.

I've played around with the AutoresizingMask of the documentView, the NSClipView, the scrollview, etc. I tried setting TranslatesAutoresizingMaskIntoConstraints to false for everything and adding constraints to set the Left, Right, and Top of the documentView equal to that of the NSClipView. When I did that, the view doesn't even seem to show up. I can't seem to figure this one out.

I've also tried doing this without using NSImageViews:

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    EventListScrollView.TranslatesAutoresizingMaskIntoConstraints = false;
    var clipView = new NSClipView
    {
        TranslatesAutoresizingMaskIntoConstraints = false
    };

    EventListScrollView.ContentView = clipView;
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Left, 1, 0));
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Right, 1, 0));
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Top, 1, 0));
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Bottom, 1, 0));

    var documentView = new NSView();
    documentView.WantsLayer = true;
    documentView.Layer.BackgroundColor = NSColor.Black.CGColor;
    documentView.TranslatesAutoresizingMaskIntoConstraints = false;
    EventListScrollView.DocumentView = documentView;
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Left, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Right, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Top, 1, 0));

    NSView lastHeader = null;
    foreach(var project in _projects)
    {
        var random = new Random();

        var imageView = new NSView();
        imageView.TranslatesAutoresizingMaskIntoConstraints = false;
        imageView.WantsLayer = true;
        imageView.Layer.BackgroundColor = NSColor.FromRgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)).CGColor;

        var xPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Left, 1, 0);
        NSLayoutConstraint yPosConstraint = null;
        if(lastHeader!=null)
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
        }
        else
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Top, 1, 0);
        }

        var widthConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Width, 1, 0);
        var heightConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, imageView, NSLayoutAttribute.Width, (nfloat)0.325, 0);

        documentView.AddSubview(imageView);
        documentView.AddConstraints(new[] { xPosConstraint, yPosConstraint, widthConstraint, heightConstraint });

        lastHeader = imageView;
    }
}
Jyosua
  • 648
  • 1
  • 6
  • 18
  • 1
    What `autoresizingMask` values did you try? I think it should work with just `.viewWidthSizable` and nothing else. Of course, you also have to set the document view's initial frame width to match the content view's bounds width. From then on, autoresizing should keep them in sync. – Ken Thomases Jun 21 '18 at 03:33
  • So if you look at that second block of code and change the document view declaration to: var documentView = new NSView(EventListScrollView.Bounds); documentView.WantsLayer = true; documentView.Layer.BackgroundColor = NSColor.Black.CGColor; documentView.AutoresizingMask = NSViewResizingMask.WidthSizable; EventListScrollView.DocumentView = documentView; and also delete the constraints for clipView, nothing draws. – Jyosua Jun 21 '18 at 04:30
  • There was a point at which I was able to get it to draw the document view within the scrollview using .viewWidthSizable, at least, but the problem was that it would not size the height correctly. The height was always set to whatever the initial frame was when the documentView was instantiated. I want the width to be sizeable like that, but my subviews actually grow/shrink in size when the width of the documentView changes, and I need the documentView's height to reflect that so that the clipview can actually be scrolled. – Jyosua Jun 21 '18 at 04:33
  • 1
    You have to set your document view's height yourself. Or use some container view (`NSCollectionView`? `NSStackView`?) that automatically adjusts itself to the number/size of its subviews. If the document view has `translatesAutoresizingMaskIntoConstraints` enabled (and you shouldn't disable it if you're putting it into a container view; the container needs to decide that), then constraints aren't going to be able to either position it nor size it. You'll get conflicts. In that case, you can only have constraints that position/size other views relative to it. – Ken Thomases Jun 21 '18 at 15:12
  • I'm a bit confused. How would I set that height? The height of my subviews (NSImageViews or even NSViews, in the more simplistic example) need to be a ratio of whatever their width is, and I need their width to fill the NSScrollView. Likewise, in order for the NSScrollView to be scrollable, I need my view's height to be a sum of the heights of the subviews after their sizes are calculated from the constraints. The only way I know how to size the subviews is via autolayout, but using AutoresizingMask for the documentView seems to stop it from resizing the height to be >= that of the subviews. – Jyosua Jun 21 '18 at 18:29
  • @KenThomases I ended up being able to set the height using autolayout. See my answer below. Thanks for the help! – Jyosua Jun 21 '18 at 22:41

3 Answers3

2

I figured it out. So, the trick to this is that the documentView will not know how big to make itself unless you pin its bottom to the last of your subviews. You can do this using visual format, but if you don't actually know what your views are until runtime, that's kinda shot. Here is the code that worked for me:

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    var clipView = EventListScrollView.ContentView;

    var documentView = new FlippedView();
    documentView.TranslatesAutoresizingMaskIntoConstraints = false;
    EventListScrollView.DocumentView = documentView;
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Left, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Right, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Top, 1, 0));

    NSView lastHeader = null;
    foreach (var project in _projects)
    {
        var imageView = new NSImageView();
        imageView.TranslatesAutoresizingMaskIntoConstraints = false;
        imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Horizontal);
        imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Vertical);
        imageView.ImageScaling = NSImageScale.ProportionallyUpOrDown;

        var xPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Left, 1, 0);
        NSLayoutConstraint yPosConstraint = null;
        if (lastHeader != null)
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
        }
        else
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Top, 1, 0);
        }

        var widthConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Width, 1, 0);
        var heightConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Height, NSLayoutRelation.LessThanOrEqual, imageView, NSLayoutAttribute.Width, (nfloat)0.360, 0);

        documentView.AddSubview(imageView);
        documentView.AddConstraint(xPosConstraint);
        documentView.AddConstraint(yPosConstraint);
        documentView.AddConstraint(widthConstraint);
        documentView.AddConstraint(heightConstraint);

        lastHeader = imageView;
        ImageService.Instance.LoadUrl($"https:{project.ProjectLogo}").Into(imageView);
    }

    if(lastHeader!=null)
    {
        var bottomPinConstraint = NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
        documentView.AddConstraint(bottomPinConstraint);
    }
}

The key is that last constraint -- the bottomPinConstraint. That allows the documentView to expand its height to fit all of the subviews.

Also, I still needed to use a view with IsFlipped = true; even though I'm setting the constraints between the NSClipView and my documentView by hand, otherwise it would ignore the constraints and pin my view to the bottom of the NSClipView. My EventListScrollView was created in Interface Builder and constraints set to make it fill the entire window it's in.

The end result is an NSScrollView with a DocumentView that fills the width of the ScrollView, but also expands vertically to fit the subviews I programmatically add to it.

Jyosua
  • 648
  • 1
  • 6
  • 18
0

I did this to make my custom view to resize by scroll view width.

    scrollView.documentView = customView
    customView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        customView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
        customView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor),
    ])

The custom view provides intrinsicContentSize in X axis.

eonil
  • 83,476
  • 81
  • 317
  • 516
0

The answer of eonil is almost perfect, but the offset gets incorrect if the system is set to always show scrollbars. The horizontal scrollbar ends up with a small offset. To fix this, use the anchor of the clipview instead:

  NSLayoutConstraint.activate([
     view.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor),
     view.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
    ])
paxos
  • 877
  • 5
  • 11