I am trying to use a custom View
in the menu bar. The View ist displayed, however, the text is not updated properly.
I have the following code:
@main
struct MenuBarTestApp: SwiftUI.App {
@StateObject var model = MainModel()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class MainModel : ObservableObject
{
private var item: NSStatusItem?
private var timer: Timer?
@Published var upper: Double = 0.0
@Published var lower: Double = 0.0
init()
{
// TODO This is a workaround. Calling this code without a small delay crashes the app immediately.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let hostingView = NSHostingView(rootView: TestView(upper: self.upper, lower: self.lower))
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
item.length = 96.0
item.button?.addSubview(hostingView)
hostingView.constraintToSuperview()
self.item = item
}
self.timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) {
_ in
self.upper = Double.random(in: 0.0...1.0)
self.lower = Double.random(in: 0.0...1.0)
}
}
}
struct TestView : View
{
var upper: Double
var lower: Double
var body: some View
{
VStack(spacing: 0.0) {
HStack {
Image(systemName: "circle.fill")
.resizable()
.frame(width: 6.0, height: 6.0)
Text(self.upper.description)
.font(Font.system(size: 8.0))
}
HStack {
Image(systemName: "circle.fill")
.resizable()
.frame(width: 6.0, height: 6.0)
Text(self.lower.description)
.font(Font.system(size: 8.0))
}
}
}
}
internal extension NSView
{
func constraintToSuperview()
{
guard let superview = self.superview else { return }
self.translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0.0).isActive = true
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0.0).isActive = true
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0.0).isActive = true
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0.0).isActive = true
}
}
The Timer
updates both variables properly. However, the texts never update in the menu bar. I thought that it may have something to do with the wrong property wrappers so I tried to use the new @Observable
macro from macOS 14. But the menu bar View
is never updated as well.
What am I missing here?
You are passing static values to your View when you do this:
TestView(upper: self.upper, lower: self.lower)
TestView
never has any reason to update because it is created once with those values.
To get your View to update, the easiest approach would be giving it a reference to the ObservableObject
you've created:
struct TestView : View
{
@ObservedObject var mainModel: MainModel
var body: some View
{
VStack(spacing: 0.0) {
HStack {
Image(systemName: "circle.fill")
.resizable()
.frame(width: 6.0, height: 6.0)
Text(mainModel.upper.description)
.font(Font.system(size: 8.0))
}
HStack {
Image(systemName: "circle.fill")
.resizable()
.frame(width: 6.0, height: 6.0)
Text(mainModel.lower.description)
.font(Font.system(size: 8.0))
}
}
}
}
And then pass the model to the View
when you create it:
TestView(mainModel: self)
Another similar approach would be keeping TestView
the same but wrapping it in something that knows about the ObservableObject
:
struct TestViewWrapper: View {
@ObservedObject var mainModel: MainModel
var body: some View {
TestView(upper: mainModel.upper, lower: mainModel.lower)
}
}
You could use that strategy if you want to keep TestView
isolated from the model.