Search code examples
swifteventsswiftuiappkit

How to make onKeyPress work with TextField in swift?


Implementing markdown text editor.

Each line of text is a TextField.

Need to switch line on 🔼 and 🔽

First attempt:

TextField(...)
 .onKeyPress(.downArrow) {
    print("down")
    return .handled
 }

This event is never fired

Second attempt:

import SwiftUI

class CustomTextField: NSTextField {
    override public var acceptsFirstResponder: Bool { true }
    
    override func keyDown(with event: NSEvent) {
        print("key down with character: \(String(describing: event.characters)) " )
        super.keyDown(with: event)
    }
}

struct LineEditor: NSViewRepresentable {
    @Binding var text: String
    
    init(_ text: Binding<String>) {
        self._text = text
    }
    
    typealias NSViewType = CustomTextField
    
    func makeNSView(context: Context) -> CustomTextField {
        let view = CustomTextField(string: text)
        view.delegate = context.coordinator
        return view
    }
    
    func updateNSView(_ nsView: CustomTextField, context: Context) {
        nsView.stringValue = text
    }
    
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: LineEditor
        
        init(_ parent: LineEditor) {
            self.parent = parent
        }
        
        func controlTextDidChange(_ obj: Notification) {
            let field = obj.object as! NSTextField
            self.parent.text = field.stringValue
        }
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
}

keyDown is never called either.

Question: how to track key press events properly in modern SwiftUI?


Solution

  • For multiline texts you need to use TextEditor.

    But in case you still need to use set of TextFields like this you need sth like the following code.

    (Just copy-paste the code. All works with any count of pre-defined TextFields :)

    import Combine
    import SwiftUI
    
    struct ContentView: View {
        @State var text1: String = "1234"
        @State var text2: String = "2345"
        @State var text3: String = "3456"
        @State var text4: String = "4567"
        
        @FocusState private var focusedField: Field?
        
        var body: some View {
            VStack {
                TextField("", text: $text1)
                    .focused($focusedField, equals: .f1)
                TextField("", text: $text2)
                    .focused($focusedField, equals: .f2)
                TextField("", text: $text3)
                    .focused($focusedField, equals: .f3)
                TextField("", text: $text4)
                    .focused($focusedField, equals: .f4)
            }
            .onAppear { keyboardUpDownSubscribe() }
        }
    }
    
    
    extension ContentView {
        func keyboardUpDownSubscribe() {
            NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
                print(aEvent.keyCode)
                
                guard focusedField != nil else { return nil }
    
                guard aEvent.specialKey != nil else { return aEvent }
                
                switch aEvent.specialKey! {
                case .downArrow:
                    focusNextField()
                case .upArrow:
                    focusPreviousField()
                default:
                    break
                }
                
                return aEvent
            }
        }
        
        private enum Field: Int, CaseIterable {
            case f1, f2, f3, f4
        }
        
        private func focusPreviousField() {
            focusedField = focusedField.map {
                Field(rawValue: $0.rawValue - 1) ?? Field.allCases.first!
            }
        }
        
        private func focusNextField() {
            focusedField = focusedField.map {
                Field(rawValue: $0.rawValue + 1) ?? Field.allCases.last!
            }
        }
    }