Set position of nspopover

2.6k views Asked by At

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"

1

There are 1 answers

11
Matthew Seaman On BEST ANSWER

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