Search code examples
swiftmacosswiftuinstextview

Handle keyboard inputs in NSTextView embedded via NSViewRepresentable? (SwiftUI/MacOS)


I'm new to SwiftUI and am utterly confused.. I managed to embed a NSTextView into my SwiftUI View and bind its text with the below code.

What I don't understand; is there a way to handle keyboard inputs to the NSTextView and change its text accordingly (e.g. CMD + R sets the text color of the selected text to red)? Is there even any way to interact with UI-Elements in SwiftUI?

"RichTextField"

struct RichTextField: NSViewRepresentable {
    typealias NSViewType = NSTextView
    
    @Binding var attributedString: NSAttributedString 

    func makeNSView(context: Context) -> NSTextView {...
    // [...]

}

View

struct EditWindow: View {
      
  @ObservedObject var model: EditEntryViewModel
  @Binding var isPresented: Bool
        
  var body: some View {    
     RichTextField(attributedString: self.$model.answer1, isEditable: true)
      // [...]
    }
}

Furthermore, I've managed to set up a menu command in the AppDelegate, but how could I use this to change the text (at a certain position) in a NSTextView of an arbitrary View?

@IBAction func setTagImportant(_ sender: Any) {
  print("setTagImportant")
}

Thanks a lot for shedding some light on this for me...


Solution

  • Ironically, immediately after finally posting this question, I found a solution; simply subclass the NSTextView and then override keyDown:

    import SwiftUI
    
    class RichTextFieldExtended: NSTextView {
        
        override func keyDown(with event: NSEvent) {
            
            if event.modifierFlags.contains(NSEvent.ModifierFlags.command) {
                
                switch event.keyCode {
                    
                case 18: // 1
                    
                    print("1 PRESSED")                
                    
                default:
                    print("keyCode \(event.keyCode) wasn't handled")
                    super.keyDown(with: event)
                }
                
            } else {
                super.keyDown(with: event)
            }
            
        }
        
    }
    

    Then include the subclassed NSTextView in the NSViewRepresentable, as follows

    struct RichTextField: NSViewRepresentable {
        typealias NSViewType = RichTextFieldExtended
        
        @Binding var attributedString: NSAttributedString
        var isEditable: Bool
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    
        func makeNSView(context: Context) -> RichTextFieldExtended {
            let textView = RichTextFieldExtended(frame: .zero)
            
            textView.textStorage?.setAttributedString(self.attributedString)
            textView.isEditable = isEditable
            textView.delegate = context.coordinator
            
            textView.translatesAutoresizingMaskIntoConstraints = false
            textView.autoresizingMask = [.width, .height] 
    
            return textView
        }
    
        func updateNSView(_ nsView: RichTextFieldExtended, context: Context) {
    //        nsView.textStorage!.setAttributedString(self.attributedString)
        }
    
        // Source: https://medium.com/fantageek/use-xib-de9d8a295757
        class Coordinator: NSObject, NSTextViewDelegate {
            let parent: RichTextField
            
            init(_ RichTextField: RichTextField) {
                self.parent = RichTextField
            }
    
            func textDidChange(_ notification: Notification) {
    
                guard let textView = notification.object as? RichTextFieldExtended else { return }
                self.parent.attributedString = textView.attributedString()
    
            }
    
        }
        
    }
    

    Cheers