swiftmacosswiftuitextfieldnswindow

TextField shortcuts in NSWindow created programmatically vs SwiftUI


I'm having a issue with a simple SwiftUI view containing a TextField.

If the View is inside a SwiftUI App > Scene > WindowGroup, the TextField in the view behave as expected and I can use shortcuts inside the TextField like Command+a to select all the text in it or copy/past.

If the View is inside a NSHostingView set as the contentView of a NSWindow created programmatically, my TextField doesn't accept shortcuts using the Command key. I can still type text or use shortcuts with CTRL but with Command the bell ring and nothing happens.

Here is the code for the working version:

import Foundation
import SwiftUI

struct MyView: View {
    @State var text = ""
    
    var body: some View {
        TextField("My textfield", text: $text).padding()
    }
}

@main
struct TestCoreDataSwiftApp: App {
    var body: some Scene {
        WindowGroup {
            MyView()
        }
    }
}

Here is the code for the not working version:

import Foundation
import Cocoa
import SwiftUI

let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

struct MyView: View {
    @State var text = ""
    
    var body: some View {
        TextField("My textfield", text: $text).padding()
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    private var window: NSWindow?
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        window = NSWindow()
        window?.contentView = NSHostingView(rootView: MyView())
        window?.makeKeyAndOrderFront(self)
        window?.makeFirstResponder(nil)
    }
}

[EDIT]

I did more tests.

If everything is created with Interface Builder (drag and drop a NSTextField inside a NSView), it also works as expected.

If we create a NSView programmatically and add a NSTextField, here again it doesn't work:

    // Not working code
    func applicationDidFinishLaunching(_ aNotification: Notification) 
    {
        NSApplication.shared.activate(ignoringOtherApps: true)
        window = NSWindow(contentRect: NSRect(x: 400, y: 400, width: 400, height: 100), styleMask: [.miniaturizable, .closable, .resizable, .titled], backing: .buffered, defer: false)
        window.center()
        window.title = "Title"
        window.contentView = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 300))
        let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 60))
        window.contentView?.addSubview(textField)
        window.makeKeyAndOrderFront(nil)
    }

I suspect it's something related to the focus or the firstResponder but I'm unable to make it work. Any help is welcome. Thanks!


Solution

  • In fact, those shortcuts are related to the application menu (NSMenu). They don't exist by default without it.
    So, if the application doesn't have the Edit menu where all this shortcuts are defined, they are simply not available in the TextField.

    In the various cases where it works, it's because both the default IB project and the Swift UI project add this menu by default.

    It's not a matter of firstResponder in this case.

    In order to make this work, the solutions are:

    1. Add the Edit menu in the application with the shortcuts (doesn't work if the menu is hidden).
    2. Manage shortcuts programmatically as suggested by VonC
    class NSHostingViewShortcuts<Content>: NSHostingView<Content> where Content : View {
        override func performKeyEquivalent(with event: NSEvent) -> Bool {
            if event.modifierFlags.contains(.command) {
                let shortcuts = [
                    "a": #selector(NSStandardKeyBindingResponding.selectAll(_:)),
                    "x": #selector(NSText.cut(_:)),
                    "c": #selector(NSText.copy(_:)),
                    "v": #selector(NSText.paste(_:))
                ]
                if event.characters != nil && shortcuts[event.characters!] != nil {
                    NSApp.sendAction(shortcuts[event.characters!]!, to: nil, from: nil)
                    return true
                }
            }
            
            return super.performKeyEquivalent(with: event)
        }
    }