Search code examples
macoscocoaautolayoutnsscrollviewxamarin.mac

NSScrollView with a matching width DocumentView


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;
    }
}

Solution

  • 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.