Reputation: 3877
I'm working on a macOS SwiftUI app. It has a "File->Open Location…" menu command that uses a Segue in IB to modally present an NSWindowController that contains an NSHostingController subclass. My subclass looks like this:
class
OpenLocationController: NSHostingController<OpenLocationView>
{
@objc
required
dynamic
init?(coder: NSCoder)
{
super.init(coder: coder, rootView: OpenLocationView())
}
}
and my view looks like this:
struct
OpenLocationView : View
{
@State private var location: String = ""
var body: some View
{
VStack
{
HStack
{
Text("Movie Location:")
TextField("https://", text: $location)
}
HStack
{
Spacer()
Button("Cancel") { /* dismiss window */ }
Button("Open") { }
}
}
.padding()
.frame(minWidth: 500.0)
}
}
I tried adding a property @Environment(\.presentationMode) var presentationMode
and calling self.presentationMode.wrappedValue.dismiss()
in the button action, but it has no visible effect.
How do I dismiss this window when the user clicks Cancel?
Upvotes: 2
Views: 1110
Reputation: 162
Upd: just checked on pre-macOS 14 and the default dismiss
action doesn‘t call performClose:
there, I‘ve updated my answer to support pre-macOS 14 deployment.
It seems the approach presenting NSHostingController
as a sheet is incorrect.
Instead, custom NSWindow
subclass should be used with contentView
set to NSHostingView
and performClose:
method overridden to dismiss the sheet using the default dismiss()
action:
struct MySwiftUIView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
Text("This is a SwiftUI View")
Button("OK") {
dismiss()
}.keyboardShortcut(.defaultAction)
Button("Cancel") {
dismiss()
}.keyboardShortcut(.cancelAction)
}
.padding()
.fixedSize()
}
}
final class MyWindow<Content: View>: NSWindow {
init(rootView: Content) {
super.init(contentRect: .zero, styleMask: [.titled, .closable, .docModalWindow], backing: .buffered, defer: false)
self.contentView = NSHostingView(rootView: rootView
// remove this if pre-macOS 14 support is not needed
.legacyOnDismiss { [weak self] in
self?.performClose(nil)
}
)
}
override func performClose(_ sender: Any?) {
guard let sheetParent else {
super.performClose(sender)
return
}
sheetParent.endSheet(self, returnCode: .alertFirstButtonReturn)
}
}
func show(completion: (() -> Void)? = nil) {
let myWindow = MyWindow(rootView: MySwiftUIView())
NSApp.mainWindow!.beginSheet(myWindow, completionHandler: completion.map { completion in
{ _ in
completion()
}
})
}
pre-macOS 14 support:
extension View {
@available(macOS, obsoleted: 14.0, message: "This needs to be removed.")
@ViewBuilder
func legacyOnDismiss(_ onDismiss: @escaping () -> Void) -> some View {
if #unavailable(macOS 14.0), let presentationModeKey = \EnvironmentValues.presentationMode as? WritableKeyPath {
// downcast a (non-writable) \.presentationMode KeyPath to a WritableKeyPath
self.environment(presentationModeKey, Binding<PresentationMode>(isPresented: true, onDismiss: onDismiss))
} else {
self
}
}
}
@available(macOS, obsoleted: 14.0, message: "This needs to be removed.")
private extension Binding where Value == PresentationMode {
init(isPresented: Bool, onDismiss: @escaping () -> Void) {
// PresentationMode is a struct with a single isPresented property and a (statically dispatched) mutating function
// This technically makes it equal to a Bool variable (MemoryLayout<PresentationMode>.size == MemoryLayout<Bool>.size == 1)
var isPresented = isPresented
self.init {
// just return the Bool as a PresentationMode
unsafeBitCast(isPresented, to: PresentationMode.self)
} set: { newValue in
// set it back
isPresented = newValue.isPresented
// and call the dismiss callback
if !isPresented {
onDismiss()
}
}
}
}
pre-macOS 12 support:
@available(macOS, obsoleted: 12.0, message: "This needs to be removed.")
struct DismissAction {
let dismiss: () -> Void
public func callAsFunction() {
dismiss()
}
}
extension EnvironmentValues {
@available(macOS, obsoleted: 12.0, message: "This needs to be removed.")
var dismiss: DismissAction {
DismissAction {
presentationMode.wrappedValue.dismiss()
}
}
}
Upvotes: 0
Reputation: 258345
Here is possible approach. Tested with Xcode 11.2 / macOS 15.0.
class
OpenLocationController: NSHostingController<OpenLocationView>
{
@objc
required
dynamic
init?(coder: NSCoder)
{
weak var parent: NSViewController? = nil // avoid reference cycling
super.init(coder: coder, rootView:
OpenLocationView(parent: Binding(
get: { parent },
set: { parent = $0 })
)
)
parent = self // self usage not allowed till super.init
}
}
struct
OpenLocationView : View
{
@Binding var parent: NSViewController?
@State private var location: String = ""
var body: some View
{
VStack
{
HStack
{
Text("Movie Location:")
TextField("https://", text: $location)
}
HStack
{
Spacer()
Button("Cancel") {
self.parent?.dismiss(nil) // if shown via NSViewController.present
// self.parent?.view.window?.performClose(nil) // << alternate
}
Button("Open") { }
}
}
.padding()
.frame(minWidth: 500.0)
}
}
Upvotes: 2