I have a menubar-only (Mac) SwiftUI app. The core of it is this:
// MyMainApp.swift
var body: some Scene {
MenuBarExtra("My App", image: "LoggedOutIcon") {
MenuBarView()
}
}
I'd like to dynamically update the image to "LoggedInIcon" whenever the user is logged in, and "LoggedOutIcon" whenever the user is logged out. That functionality is set in a (non-view) controller.
I figured the way to do this was to pass in my main controller to my app like this:
// MyMainApp.swift
@StateObject var myMainController = MyMainController()
Within that, set a published variable like this:
// MyMainController.swift
@Published var loggedIn: Bool = false
And then update the MenuBarExtra call to
// MyMainApp.swift
MenuBarExtra("My App", image: myMainController.loggedIn ? "LoggedInIcon" : "LoggedOutIcon")
The good news is: it works. The bad news is, that @StateObject var myMainController
line raises the following purple notification of doom:
Accessing StateObject's object without being installed on a View. This will create a new instance each time.
...That seems like something I should avoid.
In short: what is the best practice for updating a MenuBarExtra
icon in a SwiftUI app outside the scope of a view?
If the loggedIn
property is toggled by a view further down in your hierarchy-- e.g. in MenuBarView
:
struct MenuBarView: View {
@Binding var loggedIn: Bool
var body: some View {
Button(action: { loggedIn.toggle() }) {
Text(loggedIn ? "Log Out" : "Log In")
}
}
}
Then you can use the @AppStorage
property wrapper and in MyMainApp.Swift
:
@AppStorage("loggedIn") private var loggedIn = false
var body: some Scene {
MenuBarExtra("My App", image: loggedIn ? "LoggedInIcon" : "LoggedOutIcon") {
MenuBarView(loggedIn: $loggedIn)
}
}
If you want it to be toggled in the MyMainController
, you'd still use @AppStorage
but instead of having a @Binding
to it in a View, you'd do:
// MyMainController.swift
var loggedIn: Bool {
didSet {
UserDefaults.standard.set(loggedIn, forKey: "loggedIn")
}
}