How to handle lifecycle events for MenuBarExtra in macOS 13 SwiftUI
Asked Answered
I

3

14

I'm trying to create a simple menu bar extra in Swift UI using the new MenuBarExtra. I would like the button text in the popover to update dynamically every time the menu is open.

I'm creating the MenuBarExtra like this.

enter image description here

        MenuBarExtra("Example menu title") {
            Button("Item 1") {
              
            }
            
            Button("Item 2") {
              
            }
            
            Button("Item 3") {
              
            }
        }

I would like the button text (ie. Item 1) to change every time the menu is open. I would have expected onAppear to fire every time the menu is open, but it only fires the first time. After the initial opening of the popover, there is no clear way to detect a menu close or open event.

I have tried using the various event handling callbacks to detect the popover opening. OnAppear works for detecting the initial creation of the view while onDisappear is notably never called.

    MenuBarExtra("Example menu title") {
        VStack {
            Button("Item 1") {
                
            }
            
            Button("Item 2") {
                
            }
            
            Button("Item 3") {
                
            }
        }.onAppear() {
            print("This only prints the very first time the menu is opened")
        }
    }
Infirm answered 8/11, 2022 at 1:22 Comment(2)
I'm facing the same issue. Have you found a solution?Shult
Nope no luck unfortunately.Infirm
S
4

I happend to do some thing like this, and I find some solution like this:

I passed appDelegate use .environmentObject(appDelegate) to a MenuBarView. In the MenuBarView, use @EnvironmentObject to bind the view, and then I maintain the menu list data for the MenuBarView in AppDelegate

Some demo code:

App.swift

import SwiftUI
@main
struct MyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        MenuBarExtra("Example menu title") {
            MenuBarView().environmentObject(appDelegate)
        }
    }
}

AppDelegate.swift

import AppKit
import SwiftUI

final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
    @Published var menuItems: [String] = []

    func applicationDidFinishLaunching(_ notification: Notification) {
        // build menuItems here or somewhere
        menuItems = ["Item 1", "Item 2"]
    }
}

MenuBarView.swift

import SwiftUI
struct MenuBarView: View {
    @EnvironmentObject var appDelegate: AppDelegate
    var body: some View {
        ForEach(appDelegate.menuItems) { menuItem in
            Button(menuItem) {}
        }
    }
}

After all this setup, you can then maintain the appDelegate.menuItems in AppDelegate programmatically, and the MenuBarView will update automatically.

Sicilia answered 14/3, 2023 at 9:34 Comment(1)
applicationDidFinishLaunching gets fired once, not each time the app menu opens.Triangulate
M
4

This worked for me in xcode 15/macos 14 at least

MenuBarExtra("App", systemImage: "computermouse") {
    ContentView().onAppear {
        NotificationCenter.default.addObserver(
            forName: NSWindow.didChangeOcclusionStateNotification, object: nil, queue: nil) 
        { notification in
            print("Visible: \((notification.object as! NSWindow).isVisible)")
        }
    }
}.menuBarExtraStyle(.window)
Maricela answered 14/10, 2023 at 20:11 Comment(2)
It does also get called when you open a popover unfortunately, otherwise it works.Wedgwood
@Wedgwood that was requested by this question. Getting called each time the menu (popover) opensTriangulate
N
-1

According to Apple's Docs, MenuBarExtra conforms to Scene - this means that you can use an Environment variable of ScenePhase to call something every time the MenuBarExtra enters the foreground or background. Article source: https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-when-your-app-moves-to-the-background-or-foreground-with-scenephase. Example usage:

@main
struct AppWithMenuBarExtra: App {
@Environment(\.scenePhase) var scenePhase // <-- HERE! This will give you the values

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        MenuBarExtra(
            "App Menu Bar Extra", systemImage: "star")
        {
            StatusMenu()
.onChange(of: scenePhase) { newPhase in //<-- HERE TOO! This modifier allows you to detect change of scene.
                if newPhase == .inactive {
                    //Code for moved to inactive
                    print("Moved to inactive")
                } else if newPhase == .active {
                    //Code for moved to foreground
                    print("Moved to foreground - now active")
                    //This is where you would want to change your text
                } else if newPhase == .background {
                    //Code for moved to background
                    print("Moved to background")
                }
            }
        }
    }
}
Noel answered 8/11, 2022 at 3:47 Comment(4)
Tried this out and unfortunately its not working for the MenuBarExtra. If I set onChange on ContentView within the WindowGroup it works exactly as expected. If I pass the same ContentView to MenuBarExtra it stops printing.Infirm
Same for me, I cannot make it work unfortunately. Did you find any solution or is it just a bug?Waitress
Seems like the bindings stop working once the View is initially rendered. I even had to push an EmptyView to get lifecycle events going. Kinda frustrating for something that seems relatively direct to implement.Crippling
Problem with the above, is the MenuBar does not execute anything until clicked on. At that stage, it starts "running". I ended up adding a WindowGroup with and EmptyView just to attach lifecycle methods. Feels like something is missing here.Crippling

© 2022 - 2024 — McMap. All rights reserved.