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.