feedback-assistant / reports

Open collection of Apple Feedback Assistant reports
226 stars 2 forks source link

FB13683957: SwiftUI MenuBarExtra with `.menu` style should rerender the body view when the menu is opened #477

Open sindresorhus opened 6 months ago

sindresorhus commented 6 months ago

Description

This would be useful so that it would show fresh content when the menu is opened. For example, the current date. In AppKit land, most menu bar apps call NSMenu#removeAllItems() to recreate the menu on open. This IMHO makes sense for menu bar apps, as they are long running, and you don’t really want to keep updating them in the background when they are only used once in a while (waste of resources). You want to update them when the user actually opens the menu to the content.

Basically, in the below example, I would want it to print each time the menu is opened:

import SwiftUI

@main
struct SampleApp: App {
    var body: some Scene {
        MenuBarExtra("Click me") {
            let _ = print("UPDATE")
            Text("Hej")
        }
    }
}

Related FB13683950, although I think both proposals would be useful.

sindresorhus commented 6 months ago

Apple reply:


Thank you for your feedback.

In general, SwiftUI will call a View's body property only when it needs to. Typically, this will be due to some state that the view depends on being updated.

Therefore, it is best to think of your views as a function of state.

With the MenuBarExtra example you have provided here, there is no state being updated, and as such, SwiftUI will not call body unnecessarily, regardless of whether the menu is being re-presented.

If we were to add a dependency on state, say for example a value that updates periodically via a timer:

@Observable
final class Counter {
    static var shared = Counter()

    init() {
        let timer = Timer(timeInterval: 1, repeats: true) { _ in
            self.value += 1
        }

        RunLoop.main.add(timer, forMode: .common)
    }

    var value = 1
}

@main
struct SampleApp: App {
    var body: some Scene {
        MenuBarExtra {
            MenuContent()
        } label: {
            Text("Extra Label")
        }
    }
}

struct MenuContent: View {
    let counter = Counter.shared

    var body: some View {
        Text("Value: \(counter.value)")
    }
}

Then, we will see body called when the menu is displayed, since it is now dependent on some state that is changing.

We hope this helps to better define your desired use case.

sindresorhus commented 6 months ago

My reply:


Thanks for the reply. Much appreciated. I'm aware of the workarounds. I'm arguing that opening the menu is in fact a state change, although an external state change. Same as changing the system locale in System Settings is an external state change that does trigger a view update. Because of FB13683950, there is currently no good way to actually update the menu when it opens. FB13683950 may as well be a better solution, and I would be fine with that too.

Regarding the provided example. I believe that's an anti-pattern because the counter will cause the view to update every 1 second even when the menu is closed, so it will waste a lot of system resources. Prepend let _ = print("UPDATE") to the MenuContent body and run the app, and you will see that it keeps updating even when the menu is not shown.

dagronf commented 4 months ago

Hey mate -

I just ran into this myself and was stumped for a few hours. I think I've found a nice solution that doesn't require polling, and basically means that when the menu bar extra's view content appears you can react accordingly

The Environment has an isPresented member that toggles when a view appears or disappears.

So you can set an .onChange(of: isPresented) { ... } within your view.

struct MenuBarExtraView: View {
   @Environment(\.isPresented) var isPresented

   var body: some View {
      Button(...)
         .onChange(of: isPresented) { oldValue, newValue in
            if newValue {
               ... do something ...
            }
         }
   }
}

Anyway, hope this is useful.

dagronf commented 4 months ago

Ouch. This has suddenly stopped working - and I have no idea why.

UPDATE: isPresented only changes IF the menubarextra application is the frontmost application. For a menubarextra it is most likely not (I have additional windows in my app). Boo!

kinjo-ryura commented 1 week ago

https://damian.fyi/swift/2022/12/29/detecting-when-a-swiftui-menubarextra-with-window-style-is-opened.html