I created this SwiftUI GrowingTextViewRepresentable, which represents a UITextView. I want to maintain the selectedTextRange even after the focus shifts to another TextField/TextView. To achieve this, I tried preserving it with a Binding. I save it in textViewDidEndEditing, expecting it to remain selected when the focus changes to another text field. I then set it again in updateUIView when the view loads. However, it's not working as expected. What am I doing wrong, or how can I achieve this?
FYI: GrowingTextView is just a UITextView subclass, which increases the height when multiline text changes.
GrowingTextViewRepresentable Code:
import UIKit
import SwiftUI
import GrowingTextView
struct GrowingTextViewRepresentable: UIViewRepresentable {
let textView = GrowingTextView()
var placeHolder: String = ""
var customFont: UIFont = .systemFont(ofSize: 12)
var placeHolderColor: UIColor = .grey
var textColor: UIColor = .black
@Binding var text: String
@Binding var height: CGFloat
@Binding var selectedText: String
@Binding var selectedTextRange: UITextRange?
func makeUIView(context: Context) -> GrowingTextView {
textView.delegate = context.coordinator
textView.font = customFont
textView.placeholder = placeHolder
textView.placeholderColor = placeHolderColor
textView.textColor = textColor
textView.backgroundColor = .clear
textView.clipsToBounds = true
return textView
}
func updateUIView(_ uiView: GrowingTextView, context: Context) {
uiView.text = text
if selectedTextRange != nil && uiView.selectedTextRange != selectedTextRange {
uiView.selectedTextRange = selectedTextRange
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, placeHolder: placeHolder, height: $height, selectedText: $selectedText, selectedTextRange: $selectedTextRange)
}
class Coordinator: NSObject, GrowingTextViewDelegate {
@Binding var text: String
var placeHolder: String
@Binding var height: CGFloat
@Binding var selectedText: String
@Binding var selectedTextRange: UITextRange?
private var tempSelectedBodyTextRange: UITextRange? = nil
init(text: Binding<String>, placeHolder: String, height: Binding<CGFloat>, selectedText: Binding<String>, selectedTextRange: Binding<UITextRange?>) {
self._text = text
self.placeHolder = placeHolder
self._height = height
self._selectedText = selectedText
self._selectedTextRange = selectedTextRange
}
func textViewDidChange(_ textView: UITextView) {
// UIKit -> SwiftUI
_text.wrappedValue = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
// Fires off every time the user changes the selection.
if let selectedText = textView.selectedText, let range = textView.selectedTextRange {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self._selectedText.wrappedValue = selectedText
self._selectedTextRange.wrappedValue = range
self.tempSelectedBodyTextRange = range
}
//print(selectedText)
}
}
func textViewDidChangeHeight(_ textView: GrowingTextView, height: CGFloat) {
DispatchQueue.main.async {
self.height = height
}
}
func textViewDidEndEditing(_ textView: UITextView) {
// Save the selected range when the text view ends editing
if let range = tempSelectedBodyTextRange {
DispatchQueue.main.async {
self._selectedTextRange.wrappedValue = range
textView.selectedTextRange = range
}
}
}
}
}
extension UITextView {
var selectedText: String? {
guard let selectedRange = selectedTextRange else { return nil }
return text(in: selectedRange)
}
}
How I'm using it:
import UIKit
struct MyView: View {
@State private var body:String = ""
@State private var selectedBodyText: String = ""
@State private var selectedBodyTextRange: UITextRange? = nil
@State private var heightOfBody: CGFloat = 0
var body: some View {
GrowingTextViewRepresentable(placeHolder: "write about...",
text: body, height: $heightOfBody, selectedText: selectedBodyText, selectedTextRange: selectedBodyTextRange)
.frame(height: heightOfBody)
.frame(minHeight: 20)
.onChange(of: viewModel.selectedBodyText, perform: { newValue in
print("selectedBodyText: \(newValue)")
})
}
}
I attempted to retain the value in tempSelectedBodyTextRange and then reset it in textViewDidEndEditing, but it didn't work as expected.
makeUIViewneeds to init and return yourGrowingTextViewotherwise it will be lost, e.g.Coordinatorcannot be init with any params or the params will be lost, it must be given the latest params inupdateUIView, e.g.