1

The Problem

I want a text editor in SwiftUI that styles any instance of certain keywords, in real time as the user types. Additionally, I want a button in the toolbar to add whatever text is selected in the text editor to this list of keywords.

This is my current attempt, UIViewControllerRepresentable and a Coordinator:


struct FormTextEditorBody: UIViewRepresentable {
    @EnvironmentObject var form: FormViewModel
    @Binding var text: String
    
    let highlightKeywords: Bool
    
    init(_ text: Binding<String>, _ highlightKeywords: Bool) {
        self._text = text
        self.highlightKeywords = highlightKeywords
    }
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        if !highlightKeywords {
            uiView.text = text; return
        }
        
        let attributed = NSMutableAttributedString(string: text)
        
        attributed.addAttribute(
            NSAttributedString.Key.font,
            value: UIFont.systemFont(ofSize: 16),
            range: NSRange(0..<NSString(string: text).length)
        )
        
        attributed.addAttribute(
            NSAttributedString.Key.foregroundColor,
            value: UIColor(Color.primary),
            range: NSRange(0..<NSString(string: text).length)
        )
        
        for word in form.keywords {
            for range in text.independentRangesOf(string: word.text) {
                attributed.addAttribute(
                    NSAttributedString.Key.foregroundColor,
                    value: UIColor(Color.secondaryAccent),
                    range: NSRange(range, in: text))
            }
        }
        
        uiView.attributedText = attributed
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: FormTextEditorBody
        
        init(_ parent: FormTextEditorBody) {
            self.parent = parent
        }
        
        func textViewDidChange(_ textView: UITextView) {
            parent.text = textView.text
        }
        
        func textViewDidChangeSelection(_ textView: UITextView) {
            if textView.selectedTextRange == nil {
                return
            }
            
            let selectedString = textView.text(in: textView.selectedTextRange!) ?? ""
            
            parent.form.selectedText = selectedString
        }
    }
}

A pure SwiftUI view higher up the stack accesses form.selectedText to add it to the highlight list.

This approach almost does everything I want, but it has a bug: if I type a character anywhere into the text editor, the character is inserted and then the cursor jumps to the end of the text.

What I've tried so far

  • I think the @EnvironmentObject is somehow involved. Removing it and the associated behaviour does fix the bug, but then I have nowhere to save the selected text that it can be accessed higher up the stack.

  • I tried stripping out all the attributed string stuff and just setting uiView.text, thinking that applying the styling might be a factor. The bug was unaffected.

  • I tried moving the @EnvironmentObject a view up the stack and passing the selected text as a binding. The bug was unaffected.

  • Manually retrieving the cursor position at the start of updateUIView and reapplying it after the style update. Causes janky, unpredictable typing behaviour.

Alpheratz
  • 29
  • 2

0 Answers0