You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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)")
        })
    }
}

问题分析与解决方案

核心问题

  1. UITextRange的生命周期限制:UITextRange是和所属UITextView实例绑定的临时对象,一旦文本框失去焦点、实例被重建或复用,之前保存的UITextRange就会失效,无法直接复用。
  2. Binding传递错误:使用GrowingTextViewRepresentable时,text、selectedText等参数未用$传递Binding,导致SwiftUI状态与UIKit控件无法双向同步。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 16:00:56