Reputation: 593
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
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
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
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