如何在UITextView的UIViewRepresentable中维持selectedTextRange选中状态
问题描述
我用SwiftUI封装了GrowingTextViewRepresentable,用来包装UITextView的子类GrowingTextView(支持随多行文本自动增高)。现在需要实现:当焦点切换到其他TextField/TextView时,该文本框能保留之前的选中状态。我尝试通过Binding保存selectedTextRange,在textViewDidEndEditing中存值、updateUIView中恢复,以及用tempSelectedBodyTextRange暂存重置,均未生效。请问问题出在哪?该如何实现需求?
原代码
GrowingTextViewRepresentable代码
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) { _text.wrappedValue = textView.text } func textViewDidChangeSelection(_ textView: UITextView) { 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 } } } func textViewDidChangeHeight(_ textView: GrowingTextView, height: CGFloat) { DispatchQueue.main.async { self.height = height } } func textViewDidEndEditing(_ textView: UITextView) { 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) } }
使用示例代码
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)") }) } }
问题分析与解决方案
核心问题
UITextRange的生命周期限制:UITextRange是和所属UITextView实例绑定的临时对象,一旦文本框失去焦点、实例被重建或复用,之前保存的UITextRange就会失效,无法直接复用。- Binding传递错误:使用
GrowingTextViewRepresentable时,text、selectedText等参数未用$传递Binding,导致SwiftUI状态与UIKit控件无法双向同步。 updateUIView逻辑无效:直接比较不同实例的UITextRange没有意义,失效的UITextRange也无法恢复选中状态。
修复方案
改用NSRange保存选中位置(值类型,可稳定跨生命周期复用),修正Binding传递,调整控件实例创建逻辑。
修正后的GrowingTextViewRepresentable
import UIKit import SwiftUI import GrowingTextView struct GrowingTextViewRepresentable: UIViewRepresentable { var placeHolder: String = "" var customFont: UIFont = .systemFont(ofSize: 12) var placeHolderColor: UIColor = .gray var textColor: UIColor = .black @Binding var text: String @Binding var height: CGFloat @Binding var selectedText: String @Binding var selectedRange: NSRange? // 改用NSRange保存偏移量 func makeUIView(context: Context) -> GrowingTextView { let textView = 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) { // 避免不必要的文本赋值,防止覆盖用户输入 if uiView.text != text { uiView.text = text } // 根据NSRange恢复选中状态 guard let selectedRange = selectedRange, let startPosition = uiView.position(from: uiView.beginningOfDocument, offset: selectedRange.location), let endPosition = uiView.position(from: startPosition, offset: selectedRange.length) else { return } let newTextRange = uiView.textRange(from: startPosition, to: endPosition) if uiView.selectedTextRange != newTextRange { uiView.selectedTextRange = newTextRange } } func makeCoordinator() -> Coordinator { Coordinator(text: $text, height: $height, selectedText: $selectedText, selectedRange: $selectedRange) } class Coordinator: NSObject, GrowingTextViewDelegate { @Binding var text: String @Binding var height: CGFloat @Binding var selectedText: String @Binding var selectedRange: NSRange? init(text: Binding<String>, height: Binding<CGFloat>, selectedText: Binding<String>, selectedRange: Binding<NSRange?>) { self._text = text self._height = height self._selectedText = selectedText self._selectedRange = selectedRange } func textViewDidChange(_ textView: UITextView) { text = textView.text } func textViewDidChangeSelection(_ textView: UITextView) { guard let textRange = textView.selectedTextRange else { selectedText = "" selectedRange = nil return } // 转换UITextRange为NSRange保存 let startOffset = textView.offset(from: textView.beginningOfDocument, to: textRange.start) let length = textView.offset(from: textRange.start, to: textRange.end) selectedRange = NSRange(location: startOffset, length: length) selectedText = textView.text(in: textRange) ?? "" } func textViewDidChangeHeight(_ textView: GrowingTextView, height: CGFloat) { self.height = height } } } extension UITextView { var selectedText: String? { guard let selectedRange = selectedTextRange else { return nil } return text(in: selectedRange) } }
修正后的使用示例
import SwiftUI struct MyView: View { @State private var bodyText: String = "" @State private var selectedBodyText: String = "" @State private var selectedBodyRange: NSRange? = nil // 改为NSRange?类型 @State private var heightOfBody: CGFloat = 0 var body: some View { GrowingTextViewRepresentable( placeHolder: "write about...", text: $bodyText, // 修正Binding传递 height: $heightOfBody, selectedText: $selectedBodyText, selectedRange: $selectedBodyRange ) .frame(height: heightOfBody) .frame(minHeight: 20) .onChange(of: selectedBodyText) { newValue in print("selectedBodyText: \(newValue)") } } }
关键修复点
- 用
NSRange替代UITextRange保存选中位置,避免临时对象失效问题。 - 每次
makeUIView创建新的GrowingTextView实例,避免复用导致的状态混乱。 - 修正Binding传递错误,确保SwiftUI状态与UIKit控件双向同步。
- 移除冗余的
tempSelectedBodyTextRange和textViewDidEndEditing逻辑,依靠textViewDidChangeSelection实时保存选中状态。
内容的提问来源于stack exchange,提问作者Jayant Badlani
相关产品推荐
相关产品推荐

