Aleph
Aleph

Reputation: 593

Calling a sheet from a menu item

I have a macOS app that has to display a small dialog with some information when the user presses the menu item "Info".

I've tried calling doing this with a .sheet but can't get it to display the sheet. Code:

@main
struct The_ThingApp: App {
    private let dataModel = DataModel()
    @State var showsAlert = false
    @State private var isShowingSheet = false

    var body: some Scene {
       WindowGroup {
          ContentView()
          .environmentObject(self.dataModel)
       }
       .commands {
            CommandMenu("Info") {
                Button("Get Info") { 
                    print("getting info")
                    isShowingSheet.toggle()
                }
                .sheet(isPresented: $isShowingSheet) {
                    VStack {
                        Text("Some stuff to be shown")
                            .font(.title)
                            .padding(50)
                        Button("Dismiss",
                               action: { isShowingSheet.toggle() })
                    }
                }
            }
       }
    }
}

How would I display a sheet from a menu item?

However, if a sheet is not the way to do it (I think given the simplicity of what I need to show, it would be it), how would you suggest I do it? I tried creating a new view, like I did with the preferences window, but I can't call it either from the menu.

Upvotes: 3

Views: 551

Answers (3)

Lloyd Sargent
Lloyd Sargent

Reputation: 614

I came up with what I believe is a far simpler way by using bindings.

First create the app code (basically this is mostly the Hello World code with a few changes).

import SwiftUI

@main
struct sheetFromMacOSApp: App {
    @State var displaySheet = false
    
    var body: some Scene {
        WindowGroup {
            ContentView(displaySheet: $displaySheet)
        }
        .commands{
            CommandGroup(replacing: .appInfo) {
                Button(action: {
                    displaySheet.toggle()
                }) {
                    Text("CLICK ME")
                }
            }
         }
    }
}

This uses is a @State which is part of the menu named, ClICK ME (not very original, but sure, why not?)

What is different is that in the ContentView we pass the variable displaySheet as a parameter. This means everyone has a source of truth (one true Bool to rule them all).

Our content view looks like this.

struct ContentView: View {
    @Binding var displaySheet: Bool
    
    var body: some View {
        VStack {
            Text("Push The Button!")
                .sheet(isPresented: $displaySheet, content: {
                    Text("Hello World")
                        .frame(width: 100, height: 100)
                    Button("Dismiss", action: {displaySheet.toggle()})
                })
            
            Button("Push Me", action: {displaySheet.toggle()})
        }
        .padding()
        .frame(width: 200, height: 200)
    }
}

struct ContentView_Previews: PreviewProvider {
    @State static var displaySheet = false
    
    static var previews: some View {
        ContentView(displaySheet: $displaySheet)
    }
}

We pass in the displaySheet as a binding so everyone knows what is what. The cool thing is that you can trigger the sheet in the view as a button, OR trigger it in the menu. Either way works.

Or, if you don’t want a button, just remove it. The sheet doesn’t care.

This code has been tested with Xcode 15.4 in both macOS and iOS.

EDIT: Alternative solution, which I find cleaner, is to use an environment variable (there are plenty examples on creating a custom environment variable). If you do this assure that your code does the following:

struct CreateProject: View {
    @Environment(\.myglobal) private var myglobal
    
    var body: some View {
        @Bindable var myglobal = myglobal //<<< important line

This assures that you can make changes to your global state variable. I hope this helps anyone who has this issue.

As for PER DOCUMENT state, this may answer your question, but I have not tried it nor can I vouch for it.

Upvotes: 0

Jason Armstrong
Jason Armstrong

Reputation: 1250

This answer is a little late to the party, but will hopefully provide some guidance that will work well with multi-window apps. After watching, SwiftUI on the Mac: Build the fundamentals from WWDC20 and reviewing the Apple Developer Docs for focusedSceneValue, the best way to show a sheet while respecting multiple windows is through the use of focusedSceneValue.

If you have a lot of stuff that's dependent upon the selected/focused Scene, you could use @FocusedObject instead, similar to using @EnvironmentObject.

Set up a key to show up in the list of options when using @FocusedValue in the button:

struct ShowSheetKey: FocusedValueKey {
    typealias Value = Binding<Bool>
}

extension FocusedValues {
    var showSheet: Binding<Bool>? {
        get { self[ShowSheetKey.self] }
        set { self[ShowSheetKey.self] = newValue }
    }
}

In your ContentView create state to control your sheet's presentation and link it using the .focusedSceneValue modifier:

struct ContentView: View {
    @State private var showSheet = false

    var body: some View {
        Text("Hello, world!")
        // This will make an instance of the showSheet binding outside of the view, to any view that peels it out via @FocusedValue (similar to using @Environment)
        .focusedSceneValue(\.showSheet, $showSheet)
        .sheet(isPresented: $showSheet) {
            Text("I'm in a sheet!")
        }
    }
}

Create a button that can be used to create the menu command.

struct ShowSheetButton: View {
    @FocusedValue(\.showSheet) private var showSheet
    
    var body: some View {
        Button {
            showSheet?.wrappedValue = true
        } label: {
            Label("Show Sheet", systemImage: "eye")
        }
        .disabled(showSheet == nil)
    }
}

Finally, update your commands modifier in the Scene of your App.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(after: .newItem) {
                ShowSheetButton()
                    .keyboardShortcut("n", modifiers: [.command, .shift])
            }
        }
    }
}

Upvotes: 3

ChrisR
ChrisR

Reputation: 12125

put the sheet directly on ContentView:

@main
struct The_ThingApp: App {
    
    @State private var isShowingSheet = false
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                // here VV
                .sheet(isPresented: $isShowingSheet) {
                    VStack {
                        Text("Some stuff to be shown")
                            .font(.title)
                            .padding(50)
                        Button("Dismiss",
                               action: { isShowingSheet.toggle() })
                    }
                }
        }
        
        .commands {
            CommandMenu("Info") {
                Button("Get Info") {
                    print("getting info")
                    isShowingSheet.toggle()
                }
            }
        }
    }
}

Upvotes: 3

Related Questions