How to change state outside of a view?

221 views Asked by At

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?

1

There are 1 answers

0
James On BEST ANSWER

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")
        }
    }

If you're interested in why swift is showing you this error, there is a good discussion about it here