7

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")
        }
    }
Lukas Valine
  • 183
  • 9

2 Answers2

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")
                }
            }
        }
    }
}
  • 2
    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. – Lukas Valine Nov 08 '22 at 04:12
  • 1
    Same for me, I cannot make it work unfortunately. Did you find any solution or is it just a bug? – markus_springer Dec 26 '22 at 19:21
  • 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. – vsecades Mar 07 '23 at 02:54
  • 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. – vsecades Mar 09 '23 at 00:33
1

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.

Alex
  • 124
  • 1
  • 4