16

There are lots of examples out there of how to get NSScrollView to center its document view. Here are two examples (which are so similar that somebody is copying somebody without attribution, but the point of how is there.) http://www.bergdesign.com/developer/index_files/88a764e343ce7190c4372d1425b3b6a3-0.html https://github.com/devosoft/avida/blob/master/apps/viewer-macos/src/main/CenteringClipView.h

This is normally done by subclassing NSClipView and overriding:

- (NSPoint)constrainScrollPoint:(NSPoint)newOrigin;

But this method is deprecated in Mac OS X 10.9 +

What can we do now? Oh noes~!

uchuugaka
  • 12,679
  • 6
  • 37
  • 55

4 Answers4

26

Well, the answer is simple and nowhere near as over bloated as those are. Those do not work with double-tap magnification anyway.

This does. It just works. You can also customize your adjustments as needed.

In the @implementation you only need to implement an override of constrainBoundsRect:

- (NSRect)constrainBoundsRect:(NSRect)proposedClipViewBoundsRect {

    NSRect constrainedClipViewBoundsRect = [super constrainBoundsRect:proposedClipViewBoundsRect];

    // Early out if you want to use the default NSClipView behavior.
    if (self.centersDocumentView == NO) {
        return constrainedClipViewBoundsRect;
    }
    
    NSRect documentViewFrameRect = [self.documentView frame];
                
    // If proposed clip view bounds width is greater than document view frame width, center it horizontally.
    if (proposedClipViewBoundsRect.size.width >= documentViewFrameRect.size.width) {
        // Adjust the proposed origin.x
        constrainedClipViewBoundsRect.origin.x = centeredCoordinateUnitWithProposedContentViewBoundsDimensionAndDocumentViewFrameDimension(proposedClipViewBoundsRect.size.width, documentViewFrameRect.size.width);
    }

    // If proposed clip view bounds is hight is greater than document view frame height, center it vertically.
    if (proposedClipViewBoundsRect.size.height >= documentViewFrameRect.size.height) {
        
        // Adjust the proposed origin.y
        constrainedClipViewBoundsRect.origin.y = centeredCoordinateUnitWithProposedContentViewBoundsDimensionAndDocumentViewFrameDimension(proposedClipViewBoundsRect.size.height, documentViewFrameRect.size.height);
    }

    return constrainedClipViewBoundsRect;
}


CGFloat centeredCoordinateUnitWithProposedContentViewBoundsDimensionAndDocumentViewFrameDimension
(CGFloat proposedContentViewBoundsDimension,
 CGFloat documentViewFrameDimension )
{
    CGFloat result = floor( (proposedContentViewBoundsDimension - documentViewFrameDimension) / -2.0F );
    return result;
}

In the @interface just add one single property. This allows you to not use centering. As you can imagine, there may be conditional logic you want to turn centering off at times.

@property BOOL centersDocumentView;

Also, be sure to set this BOOL to YES or NO in your override of

initWithFrame and initWithCoder:

so you'll have a known default value to work from.

(remember kids, initWithCoder: allows you to do the needful and set a view's class in a nib. Don't forget to call super prior to your stuff!)

Of course if you need to support anything earlier than 10.9 you'll need to implement the other stuff.

(though probably not nearly as much as others have...)

EDIT: As noted by others, Apple has sample code in Swift (albeit from 2016 so it might not be the current Swift :D) at https://developer.apple.com/library/archive/samplecode/Exhibition/Listings/Exhibition_CenteringClipView_swift.html

uchuugaka
  • 12,679
  • 6
  • 37
  • 55
  • `No visible @interface for 'NSScrollView' declares the selector 'constrainBoundsRect:'` – Daniel Node.js Oct 27 '14 at 21:09
  • Edited to simplify, by moving constrainedClipViewBoundsRect = [super constrainBoundsRect:proposedClipViewBoundsRect]; ahead of the conditional guard, since super is called in either condition. – uchuugaka Apr 11 '15 at 03:15
  • This is super helpful! I wish it included the code for "initWithCoder: allows you to do the needful and set a view's class in a nib" because I'm struggling to figure that part out. – Ty Jacobs Oct 02 '16 at 20:44
  • Yeah... trouble is initWithCoder: is not a one-size fits all when you do replacements or substitutions. But do see the section *Making Substitutions During Coding* in the docs at https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Archiving/Articles/codingobjects.html – uchuugaka Oct 05 '16 at 08:26
  • -2.0F should probably be 2.0F. On my computer the extra minus gives wrong result. – Bob Ueland Dec 22 '16 at 13:55
  • You must not have read many Core Foundation style function names yet. :) – uchuugaka Jan 13 '18 at 17:23
  • Why divide by -2.0F (i.e.., why negative)? – wcochran Apr 16 '18 at 14:29
  • IIRC, flippedness. but this used to work. I'll update it later. – uchuugaka Apr 17 '18 at 02:18
  • Yes, Flippedness. Setting both clipview and document view (which may be a container for something else really) to flipped YES, in IB, selecting the document view and the size inspector, you change the top strut to be a bottom strut. Alternatively, you can set those in code. – uchuugaka Apr 17 '18 at 02:31
25

Here working class for swift

class CenteredClipView:NSClipView
{
    override func constrainBoundsRect(proposedBounds: NSRect) -> NSRect {

        var rect = super.constrainBoundsRect(proposedBounds)
        if let containerView = self.documentView as? NSView {

            if (rect.size.width > containerView.frame.size.width) {
                rect.origin.x = (containerView.frame.width - rect.width) / 2
            }

            if(rect.size.height > containerView.frame.size.height) {
                rect.origin.y = (containerView.frame.height - rect.height) / 2
            }
        }

        return rect
    }
}
pravdomil
  • 2,961
  • 1
  • 24
  • 38
  • It should be noted however that your Swift implementation is a bit simpler and different in that it doesn't make it optional. – uchuugaka Apr 11 '15 at 03:18
  • I should clarify, I made it an opt in flag, (avoiding conflating optionals...) because in some conditions you might want default behavior. – uchuugaka Feb 14 '16 at 15:19
  • I must be overlooking something here as it doesn't work. My document is a graph. The DocumentView starts very small (one node) and it expands in size. The view seem to stay in the bottom left corner. I assume that there are some Autolayout settings that might be problematic. Any ideas? I tried to mark the leading and bottom constraints as "remove at run time" which didn't help – Wizard of Kneup Jun 13 '17 at 21:22
  • Actually, this function is never ever called. I did change the type of the ClipView to this class. So, how can this be? – Wizard of Kneup Jun 15 '17 at 18:05
  • Ok, for some reason did I have to tick the "inherit from target" box when I set the custom class. – Wizard of Kneup Jun 15 '17 at 19:02
  • 6
    Apple wrote a subclass not dissimilar to this which you can find here: https://developer.apple.com/library/archive/samplecode/Exhibition/Listings/Exhibition_CenteringClipView_swift.html which worked better for me. – Jay Feb 24 '19 at 20:28
2

1) make sure the view (documentView) directly under the clip view has no constraints to the clip view! (if so, check, remove at build time)

2) subclass NSClipView

@implementation CenterClipView

- (NSRect)constrainBoundsRect:(NSRect)proposedClipViewBoundsRect {

    NSRect rect = [super constrainBoundsRect:proposedClipViewBoundsRect];
    NSView * view = self.documentView;

    if (view) {

        if (rect.size.width > view.frame.size.width) {
            rect.origin.x = (rect.size.width - view.frame.size.width) / 2.;
        }

        if(rect.size.height > view.frame.size.height) {
            rect.origin.y = (rect.size.height - view.frame.size.height) / 2.;
        }
    }

    return rect;
}

@end

3) change the NSClipView to your subclass

Borzh
  • 5,069
  • 2
  • 48
  • 64
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
  • It **really really should not** have any, because you should be the owner of this, but if you want to check this, also be sure to check the SDK version. Constraints no longer get removed, but deactivated. – uchuugaka Feb 14 '16 at 15:17
  • actually it may be important to have the width and height constraint set for the documentView, but to remove the constraints linked with the clipView itself – Peter Lapisu Feb 14 '16 at 15:57
  • Yes, you can do this best by implementing the document view as a class that has settable or calculable values that are returned from your implementation of `intrinsicContentSize` which is a method that provides h & w constraints and their constants in AppKit. The most flexible way is to have a document view class that can base this on the h & w of a subview that is the actual displayed contents provided by a view controller. It's a lot to set up 1 time then reuse easily. – uchuugaka Feb 15 '16 at 02:14
1

The answer above by uchuugaka works very well and, as pointed out, it is very much simpler than older solutions.

The code above calls the function centeredCoordinateUnitWithProposedContentViewBoundsDimensionAndDocumentViewFrameDimension(), but the source for this isn't provided.

The function is trivial, but for completeness here's an an implementation:

CGFloat centeredCoordinateUnitWithProposedContentViewBoundsDimensionAndDocumentViewFrameDimension( CGFloat clipDimension, CGFloat docDimension)
{
    CGFloat newOrigin;
    newOrigin = roundf((docDimension - clipDimension) / 2.0);
    return newOrigin;
}
Dave Robertson
  • 431
  • 3
  • 6