Search code examples
swiftcocoacocoa-bindingsnspopupbutton

NSPopUpButton - Weird behavior when populating dynamic content with Cocoa Binding


I'm trying to create a NSPopUpButton with dynamic contents, this is my design:

+-------------+
| None        | <-- Static
| Last Item   | <-- Static
|-------------| <-- Separator
| History:    | <-- Dynamic: "History:" / "No History"
| ...         | <-- Dynamic
+-------------+

And here's my code for ViewController.swift:

class ViewController: NSViewController, NSMenuDelegate {

    @objc dynamic var contents: [String] = ["None", "Last Item", ""]
    @objc dynamic var selectedIndex: Int = 0

    func updateContent() {
        // update contents array
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        updateContent()
        // other code
    }

    func menuNeedsUpdate(_ menu: NSMenu) {
        for (index, item) in menu.items.enumerated() {
            if item.title == "" {
                menu.items[index] = .separator()
            } else if item.title == "History:" || item.title == "No History" {
                menu.items[index].isEnabled = false
            }
        }
    }

    func menuDidClose(_ menu: NSMenu) {
        print(selectedIndex)
    }
}

I did the Cocoa Bindings with the interface builder. My NSPopUpButton's Content Values is bound to property contents, Selected Index is bound to selectedIndex. I set the ViewController object to be the delegate for NSPopUpButton's embedded NSMenu.

So, there is no problem with NSPopUpButton's content, but it checks whatever item I select in the NSPopUpButton and left them checked even if I select something else, eventually it becomes something like this:

Unexpected multiple selections with NSPopUpButton

And also if I open the menu (NSPopUpButton) and directly close it by not selecting any item in the menu (click anywhere other than the menu), it automatically selects the first item ("None") regardless of the previously selected item.

Automatically selected 'None' item

Then, I decided to monitor the value for selectedIndex after closing the menu by implementing menuDidClose(_:), it turns out selectedIndex is exactly what I selected previously (which is correct). This problem persists even after I deleted the binding for selectedIndex.

This is really weird and doesn't make any sense. Can anyone explain what is happening with this? And how can I properly populate a NSPopUpButton with a mixture of static and dynamic content?


Solution

  • The issue is caused by menu.items[index] = .separator(). It replaces the item array of the menu. The itemArray property of the popup button points to the item array of the menu and this property is not adjusted. The popup button can't find the menu item to switch off the check mark. Replace the menu item with

    menu.removeItem(at: index)
    menu.insertItem(NSMenuItem.separator(), at: index)
    

    Or put a separator item in the menu in IB and use the Content Placement Tag setting of the binding to insert the bound items below the separator item.