swiftmacosswiftuinsmenu

How do I detect if an NSMenu is closed?


I started down this rabbit hole because a SwiftUI button, designated to pop up a menu, with a .buttonStyle(BorderlessButtonStyle()) style wasn't behaving correctly when switching from a light to a dark mode.

Frustrated, I decided to just make my own button. I noticed the SwiftUI button would sort of toggle when popping up the menu and so I want this behaviour for my button as well. Therefore, I need to find out when an NSMenu is closed.

I've tried the answer suggested here ⤵︎

class NSMenuDelegateModified: NSObject, NSMenuDelegate, ObservableObject {

    var menu: NSMenu

    @Published var isVisible: Bool?

    internal init(menu: NSMenu) {
        self.menu = menu
        super.init()
        self.menu.delegate = self
    }

    func menuDidClose(_ menu: NSMenu) {
        isVisible = false
    }

    func menuWillOpen(_ menu: NSMenu) {
        isVisible = true
    }
}

Now this will tell me if the menu is visible from within the class, but when I try to print out .isVisible on an instantiated object of the class, it only returns false.

Is there a way to figure this out?


Solution

  • So I figured out why I can't get true out of .isVisible outside of the class! It's because I use menu.popUp(...) to open up my menu.

    It turns out that this function pauses execution for some parts of the app or perhaps the whole main thread (I'm really not sure) until it can return a state ⤵︎

    true = user selected something from the menu
    
    false = user hasn't selected anything and the menu closed
    

    You can see more details on Apple's documentation.


    Solution

    With this in mind, it made everything a lot easier! To toggle the button I could simply change the color after calling menu.popUp(...).

    This would mean that when the menu was popped up, the color wouldn't change again until a state was returned from the function and execution was resumed!

    Updated Answer

    It looks like I messed up the functionality and it was more complicated than I anticipated. Here is what I ended up with below ⤵︎

    .opacity(pressed ? 1 : 0.6)
    .inactiveWindowTap { pressed in
    
        if popped == nil {
    
            // Only register mouse ups
            if !pressed {
                popped = menu.popUp(...)
                // Execution pauses here and waits till a state is returned
            }
    
            self.pressed = pressed
        }
    
        else if !pressed {
            popped = nil
        }
    }
    .whenHovered { hovering in
        if hovering == false, popped != nil {
            popped = nil
        }
    }
    

    This solution is pretty specific to my needs so I hope what is said here still helps someone. Hopefully, someone can come up with a better answer than this.

    Old Answer (This is incorrect and doesn't work as intended)

    .opacity(pressed ? 1 : 0.6)
    .inactiveWindowTap { pressed in
    
        // Only register mouse ups
        if !pressed {
            // You can use the bool returned from this function but you don't have to.
            menu.popUp(...) // Execution pauses here until a state is found.
        }
    
        self.pressed = pressed
    }