How to change NSPopover background color include triangle part?

Solution 1:

It's actually much simpler and you won't need private API.

Make the root view of your view controller a custom class

@implementation MyPopoverRootView

-(void)viewDidMoveToWindow
{
     NSView * aFrameView = [[self.window contentView] superview];
     MyPopoverBackgroundView * aBGView  =[[MyPopoverBackgroundView alloc] initWithFrame:aFrameView.bounds];
     aBGView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
     [aFrameView addSubview:aBGView positioned:NSWindowBelow relativeTo:aFrameView];
     [super viewDidMoveToWindow];
}

@end

Your background view just draws the desired color in its bounds.

@implementation MyPopoverBackgroundView

-(void)drawRect:(NSRect)dirtyRect
{
    [[NSColor whiteColor] set];
    NSRectFill(self.bounds);
}

@end

Solution 2:

Swift 3

override func viewDidMoveToWindow() {

    guard let frameView = window?.contentView?.superview else {
        return
    }

    let backgroundView = NSView(frame: frameView.bounds)
    backgroundView.wantsLayer = true
    backgroundView.layer?.backgroundColor = .white // colour of your choice
    backgroundView.autoresizingMask = [.viewWidthSizable, .viewHeightSizable]

    frameView.addSubview(backgroundView, positioned: .below, relativeTo: frameView)

}

If you want to change only the background colour of the popover (including the triangle/arrow), I figured that you don't need to create a subclass of NSView. A layer-backed NSView with a background colour should suffice.

Also, you don't need to call super.viewDidMoveToWindow() because its default implementation does nothing.

Solution 3:

Thanks to Stefanf I got this working. Here is a Swift version of the View code. As noted, this should be the class for the View set as your NSPopOver contentView.

class PopoverContentView:NSView {
    var backgroundView:PopoverBackgroundView?
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        if let frameView = self.window?.contentView?.superview {
            if backgroundView == nil {
                backgroundView = PopoverBackgroundView(frame: frameView.bounds)
                backgroundView!.autoresizingMask = NSAutoresizingMaskOptions([.ViewWidthSizable, .ViewHeightSizable]);
                frameView.addSubview(backgroundView!, positioned: NSWindowOrderingMode.Below, relativeTo: frameView)
            }
        }
    }
}



class PopoverBackgroundView:NSView {
    override func drawRect(dirtyRect: NSRect) {
        NSColor.redColor().set()
        NSRectFill(self.bounds)
    }
}

Solution 4:

I faced the same problem, but I'm trying to avoid adding third party UI elements to my project, so I looked further. It seems if you override drawRect: in the view of your popover's contentViewController with setting a color like:

[[NSColor whiteColor] setFill];
NSRectFill([self bounds]);

then you'll end up having a popover with white background, except the triangle/arrow that connects it to the rect it is relative to. For solving that, you have to access the popover's border view which happened to contain the arrow:

NSView* borderView = [self.view.window valueForKeyPath:@"_borderView"];

I know, it is a private API, but if your goals not include submitting your app to the App Store, this is the easiest way to go. Now you can override the drawRect: for this view as well. To avoid problems like having the private _borderView property renamed with an SDK update, I suggest to assert for the borderView's existence before referencing it.

NSAssert(borderView, @"_borderView does not exist");