Make NSView NOT clip subviews outside of its bounds
Asked Answered
G

7

26

Is it possible to make an NSView not clip its subviews that are outside of the bounds? On iOS I would simply set clipsToBounds of my UIView no NO. But NSView doesn't have such a property. I tried experimenting with wantsLayer, masksToBounds, wantsDefaultClipping, but all of these seem to only change the clipping of the drawRect method, not the subviews.

Glucoside answered 22/7, 2013 at 16:45 Comment(0)
G
7

I was able to solve this by overriding wantsDefaultClipping of the subviews to return NO.

Glucoside answered 22/7, 2013 at 17:20 Comment(2)
Tested this with an NSTextField inside of a NSButton and it didn’t worked.Brew
This was the accepted answer in 2013 but wantsDefaultClipping no longer work as it should. The later answer below (2018) with CALayers should now be the accepted way of doing this.Delwyn
S
19

The behaviour around this seems to have changed. You just need to set the view's layer to not mask to bounds.

view.wantsLayer = true
view.layer?.masksToBounds = false
Sasha answered 6/11, 2018 at 16:44 Comment(0)
L
15

After 5 hours of struggling I've just achieve it. Just change class of any NSView in .storyboard or .xib to NoClippingView and it will NOT clip any of it's subviews.

class NoClippingLayer: CALayer {
    override var masksToBounds: Bool {
        set {

        }
        get {
            return false
        }
    }
}
    
class NoClippingView: NSView {
    override var wantsDefaultClipping: Bool {
        return false
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        wantsLayer = true
        layer = NoClippingLayer()
    }
}

Why do I override masksToBounds in NoClippingLayer? Because some native AppKit classes change this property of all sublayers in runtime without any warnings. For example, NSCollectionView do this for views of it's cells.

Licha answered 10/3, 2018 at 10:15 Comment(1)
And if you're not using .xib or want to avoid the step, simply add open override func makeBackingLayer() -> CALayer { return NoClippingLayer() } to NoClippingViewAnstice
G
7

I was able to solve this by overriding wantsDefaultClipping of the subviews to return NO.

Glucoside answered 22/7, 2013 at 17:20 Comment(2)
Tested this with an NSTextField inside of a NSButton and it didn’t worked.Brew
This was the accepted answer in 2013 but wantsDefaultClipping no longer work as it should. The later answer below (2018) with CALayers should now be the accepted way of doing this.Delwyn
H
5

If you are using autolayout, override alignmentRectInsets to expand the clipping area, making it larger than the alignment rectangle. This example gives a space of 50 on all sides:

override var alignmentRectInsets: NSEdgeInsets {
    return NSEdgeInsets(top: 50.0, left: 50.0, bottom: 50.0, right: 50.0)
}
Homeopathist answered 8/3, 2016 at 19:44 Comment(1)
This expands the clipping area by increasing the view's frame. If you want the frame to remain the same, but increase the clipping area (as I think we're trying to do here), you'll have adjust all of your constraints to compensate.Wharton
D
0

wantsDefaultClipping method works... both the parent & the child need to override wantsDefaultClipping.

Once you go that route, however, cleaning up after yourself is cumbersome.

Here's my solution for attaching text to an NSProgressBar:

Thanks to BGHUDAppKit and stackoverflow

@implementation BGHUDOverlayTextField
- (BOOL) wantsDefaultClipping { return NO; } // thanks stackoverflow.com
@end


@interface BGHUDProgressIndicator : NSProgressIndicator {

    NSBezierPath *progressPath;
    NSString *themeKey;
   NSString *LayerKey; 
   BGHUDOverlayTextField *customTitleField;
   BOOL hiding;
}

@implementation BGHUDProgressIndicator
- (BOOL) wantsDefaultClipping { return (customTitleField == nil); } // thanks stackoverflow.com
- (void) setCustomTitle: (NSMutableAttributedString *)iTitle
{
   if ( !customTitleField )
   {
      NSRect r = [self bounds];
      r.size.height += 10;
      customTitleField = [[BGHUDOverlayTextField alloc] initWithFrame:r];
      [self addSubview: customTitleField];
   }

   [customTitleField setAttributedStringValue:iTitle];
}

- (void)setHidden:(BOOL)flag
{
   if ( customTitleField && flag && !hiding )
   {
      hiding = YES;
      NSRect fr = [self bounds];
      NSRect eraseFr = fr;
      eraseFr.size = [customTitleField frame].size;
      [self displayRectIgnoringOpacity:fr];
   }
   else if ( !flag )
      hiding = NO;
   [super setHidden:flag];
}

- (void) drawRect: (NSRect)fr
{
   if ( customTitleField )
   {
      fr = [self bounds];
      NSRect eraseFr = fr;
      eraseFr.size = [customTitleField frame].size;
      [[NSColor normalSolidFill] set];
      NSRectFill( eraseFr );
      if ( hiding )
      {
         [customTitleField setAttributedStringValue:nil];
         NSRectFill( eraseFr );
         return;
      }
      [NSGraphicsContext saveGraphicsState];
      NSRectClip(fr);
   }
   [super drawRect:fr];

   if ( customTitleField )
   {
      [NSGraphicsContext restoreGraphicsState];
   }
}
Desiraedesire answered 15/11, 2013 at 1:9 Comment(0)
W
0

I couldn't get any of these solutions to work. I think you may have to just change your view hierarchy so that views don't draw outside of their frame. You can create an intermediary view that doesn't do any drawing but has a larger frame to allow for a larger area.

Wharton answered 2/8, 2017 at 22:36 Comment(0)
L
0

macOS 14

NSView has a new property: clipsToBounds, similar to the one on UIView for iOS.

Swift

view.clipsToBounds = true

Objective-C

[view setClipsToBounds:YES];
Lauro answered 1/10, 2023 at 14:26 Comment(1)
Objective-C: view.clipsToBounds = YES;.Causalgia

© 2022 - 2024 — McMap. All rights reserved.