SwiftUI: Missing EnvironmentObject?

Hi!
Based on stackoverflow I wanted to get information about the Window frame back into a SwiftUI View on macOS.

So I added the environmentObject:

    let contentView = ContentView()
      .environmentObject(windowInfo)

into AppDelegate.swift -> func applicationDidFinishLaunching and added the EnvironmentObject:

struct ContentView: View {
    @EnvironmentObject var windowInfo: WindowInfo

    var body: some View {
        Text("windowInfo: \(windowInfo.frame.debugDescription)")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

Unfortunately this does not work. I get the error

MissingEnvironmentObjectError: Missing EnvironmentObject
EnvironmentObject crashed due to missing environment of type: WindowInfo. To resolve this add `.environmentObject(WindowInfo(...))` to the appropriate preview.

Any clue what's wrong? What do I miss here?

PS. Use Xcode 12.5.1 on macOS 11.5.1

AFAICT .sheet modifier doesn't pass along its environment so if you view hierarchy is presented by a sheet modifier you have to manually replay it.

You should ask this on Apple's forum as this is a SwiftUI question.

1 Like

I see. SwiftUI questions belong to the developer forum. Too bad.
This public forum is IMHO a much better place…

I looked at the developer forum and tried a little. After some changes it works now.
My changes are

let windowInfo = WindowInfo()

is now global and no longer in the AppDelegate class.

In struct ContentView_Previews: PreviewProvider I added the environmentObject to the View, too:

   ContentView()
        .environmentObject(windowInfo)
1 Like

Pay attention to the error message. It specifically mentions the word "preview", which means you are experiencing this issue when using an Xcode preview. You should've mentioned this in your question. When you preview a view (in this case, ContentView), only that view and all of its subviews are invoked; your app delegate is not involved at all. For a preview, the entry point is the previews property. It's within that property that you must inject your environment objects. Don't make windowInfo global; that's bad practice. Instead, declare it as a StateObject in your preview provider:

struct ContentView_Previews: PreviewProvider {
    
    @StateObject static var windowInfo = WindowInfo()

    static var previews: some View {
        ContentView()
            .environmentObject(windowInfo)
    }
}
1 Like

This is incorrect. .sheet does receive environmentObject. It's fixed since Xcode 12 beta 4

1 Like

Don't make windowInfo global; that's bad practice.

I fully agree here. This was just the only way I got it working. I will try your recommendations tomorrow.

Many thanks for your feedback.

Thanks I had no idea this was fixed !

It doesn't work.

struct ContentView_Previews: PreviewProvider {
    @StateObject var windowInfo = WindowInfo()
    …

leads to an error:
Instance member 'windowInfo' cannot be used on type 'ContentView_Previews'

Also I need to set the windowInfo contents in App Delegate…

So I cannot get use your recommended declaration of windowInfo.

This issue can be solved via static:

struct ContentView_Previews: PreviewProvider {
    @StateObject static var windowInfo = WindowInfo()

But still I have no clue how to get the window information back here, since the data is just accessible in App Delegate…

My next try was to expand the App Delegate to get the Window information which is declared there:

    let windowInfo = WindowInfo()

The goal is to access this class instance via a delegate function of NSApplication.shared.delegate.

extension AppDelegate {
    @objc public func getWindowInfo() -> WindowInfo {
        return self.windowInfo
    }
}

This does not work because there is an error

Method cannot be marked @objc because its result type cannot be represented in Objective-C

Is there a simple example anywhere? My SwiftUI book explains e.g environmentObject but I miss how to use it as intended…

Finally I got it working. Here is a summary in case others have this problem, too:
I define a custom class to hold the needed information:

class WindowInfo: ObservableObject {
    @Published var windowFrame: CGRect = .zero
    @Published var screenFrame: CGRect = .zero
}

Now I create the windowInfo inside from class AppDelegate :

    let windowInfo = WindowInfo()

and the class properties are updated in func applicationDidFinishLaunching.

And the ContentView_Previews does work like this

struct ContentView_Previews: PreviewProvider {
    static let windowInfo = (NSApplication.shared.delegate as! AppDelegate).windowInfo
    
    static var previews: some View {
        ContentView()
            .environmentObject(windowInfo)
    }
}

The main issue here was that I cannot use AppDelegate directly.

windowInfo should still be a StateObject:

@StateObject static var windowInfo = (NSApplication.shared.delegate as! AppDelegate).windowInfo
1 Like

Ah. Yes, you're right. I overlooked that after it did work.
Now I use

@StateObject static var windowInfo = (NSApplication.shared.delegate as! AppDelegate).windowInfo