swiftmacosnsviewcontrollernspopover

Set position of nspopover


I have a view controller (A), which will show another viewcontroller (B) as a popover.

In my VC (A) is an NSButton with this IBAction:

self.presentViewController(vcPopover, asPopoverRelativeTo: myButton.bounds, of: myButton, preferredEdge: .maxX, behavior: .semitransient)

The result:

enter image description here

now I would like to change the position of my popover - I would like to move it up.

I tried this:

let position = NSRect(origin: CGPoint(x: 100.0, y: 120.0), size: CGSize(width: 0.0, height: 0.0))
self.presentViewController(vcPopover, asPopoverRelativeTo: position, of: myButton, preferredEdge: .maxX, behavior: .semitransient)

But the position does not change

ANOTHER EXAMPLE

enter image description here

I have a segmented control. If you click on segment "1" a popover will be shown (same code like above). But the arrow pointed to segment "2" instead to segment "1"


Solution

  • First, ensure your popover is really an NSPopover and not simply an NSViewController. Assuming the view controller you want to wrap in the popover has a storyboard id of "vcPopover", getting the content vc would look like:

    let popoverContentController = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "vcPopover")) as! NSViewController
    

    Then, wrap it in a popover:

    let popover = NSPopover()
    popover.contentSize = NSSize(width: 200, height: 200) // Or whatever size you want, perhaps based on the size of the content controller
    popover.behavior = .semitransient
    popover.animates = true
    popover.contentViewController = popoverContentController    
    

    Then, to present, call show(relativeTo:of:preferredEdge:):

    vcPopover.show(relativeTo: myButton.bounds, of: myButton, preferredEdge: .maxX)
    

    This should update the position of the popover.

    Update: You are likely using an NSSegmentedControl, which means you need to pay special attention to the rect you pass in show. You need to pass a bounds rect within the segmented control's coordinate system that describes the area of the segment. Here's a detailed example:

    // The view controller doing the presenting
    class ViewController: NSViewController {
    
        ...
    
        var presentedPopover: NSPopover?
    
        @IBAction func selectionChanged(_ sender: NSSegmentedControl) {
            let segment = sender.selectedSegment
    
            if let storyboard = storyboard {
                let contentVC = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("vcPopover")) as! NSViewController
    
                presentedPopover = NSPopover()
                presentedPopover?.contentSize = NSSize(width: 200, height: 200)
                presentedPopover?.behavior = .semitransient
                presentedPopover?.animates = true
                presentedPopover?.contentViewController = contentVC
            }
    
            presentedPopover?.show(relativeTo: sender.relativeBounds(forSegment: segment), of: sender, preferredEdge: .minY)
        }
    
    }
    
    extension NSSegmentedControl {
    
        func relativeBounds(forSegment index: Int) -> NSRect {
            // Assuming equal widths
            let segmentWidth = bounds.width / CGFloat(segmentCount)
    
            var rect = bounds
            rect.size.width = segmentWidth
            rect.origin.x = rect.origin.x + segmentWidth * CGFloat(index)
    
            return rect
        }
    
    }
    

    Notice that the extension to NSSegmentedControl calculates an approximate rectangle for the segment using the width. This method assumes equal widths and does not account for borders. You may modify this method to account for what you need. Information about getting the frame of a segment for iOS (which is similar) can be found here.

    This example is verified as working correctly as long as a view controller exists in the same storyboard with a storyboard identifier of "vcPopover".