iOS开发:如何识别用户点击UITextView/TextEditor的行号?
如何判断UITextView/SwiftUI TextEditor中用户点击的行号(支持富文本)
我来帮你解决这个问题——不管是UIKit里的UITextView还是SwiftUI的TextEditor,都有办法准确获取用户点击的行号,而且完全适配你用Attributed Strings设置不同样式的场景,下面分两种情况详细说明:
一、UITextView 实现方案
1. 核心思路:借助UITextViewDelegate和LayoutManager
当用户点击文本时,textViewDidChangeSelection代理方法会触发,我们可以在这里获取点击位置的字符偏移量,然后通过UITextView的layoutManager(负责文本布局的核心类)计算出对应的行号——这个方法会自动考虑富文本带来的行高、换行差异,比单纯按\n分割文本准确得多。
直接上代码示例:
class YourViewController: UIViewController, UITextViewDelegate { @IBOutlet weak var textView: UITextView! // 存储每行文本的数组(可选,根据你的需求维护) private var textLines: [String] = [] override func viewDidLoad() { super.viewDidLoad() textView.delegate = self // 初始化富文本示例 setupAttributedText() // 同步初始化行数组 updateTextLines() } private func setupAttributedText() { let attrText = NSMutableAttributedString(string: "第一行\n第二行(加粗样式)\n第三行(斜体小字号)") // 给第二行加粗 attrText.addAttributes([.font: UIFont.boldSystemFont(ofSize: 17)], range: NSRange(location: 4, length: 7)) // 给第三行设置斜体小字号 attrText.addAttributes([.font: UIFont.italicSystemFont(ofSize: 13)], range: NSRange(location: 12, length: 8)) textView.attributedText = attrText } // 代理方法:用户点击/选中文本时触发 func textViewDidChangeSelection(_ textView: UITextView) { // 确保选中的是单个位置(用户点击时的情况) guard let selectedRange = textView.selectedTextRange, let startPos = selectedRange.start as? UITextPosition else { return } // 获取点击位置对应的字符偏移量 let charOffset = textView.offset(from: textView.beginningOfDocument, to: startPos) let layoutManager = textView.layoutManager let textContainer = textView.textContainer // 计算点击位置所在的行号(注意:这里返回的行号从0开始) let glyphIndex = layoutManager.glyphIndexForCharacter(at: charOffset) let lineRect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil) let lineNumber = layoutManager.numberOfLines(for: textContainer, upTo: lineRect.origin.y) // 获取该行的文本内容 let lineCharRange = layoutManager.characterRange(forGlyphRange: layoutManager.glyphRange(for: lineRect), actualGlyphRange: nil) let lineText = (textView.attributedText.string as NSString).substring(with: lineCharRange) // 输出结果(或者执行你需要的操作,比如打印该行) print("用户点击了第 \(lineNumber + 1) 行,内容:\(lineText)") // 如果用数组存储,直接通过行号取对应元素 if lineNumber < textLines.count { let clickedLine = textLines[lineNumber] print("数组中对应的行内容:\(clickedLine)") } } // 同步更新行数组(文本变化时调用) func textViewDidChange(_ textView: UITextView) { updateTextLines() } private func updateTextLines() { textLines.removeAll() let fullText = textView.attributedText.string as NSString let layoutManager = textView.layoutManager let textContainer = textView.textContainer var currentOffset = 0 let totalChars = fullText.length while currentOffset < totalChars { let glyphIndex = layoutManager.glyphIndexForCharacter(at: currentOffset) let lineRect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil) let lineCharRange = layoutManager.characterRange(forGlyphRange: layoutManager.glyphRange(for: lineRect), actualGlyphRange: nil) let lineText = fullText.substring(with: lineCharRange).trimmingCharacters(in: .newlines) textLines.append(lineText) currentOffset = lineCharRange.location + lineCharRange.length } } }
2. 和你的数组存储方案适配
上面的代码已经包含了数组同步逻辑——每当文本变化时,textViewDidChange会调用updateTextLines,通过layoutManager把当前文本分割成真实的行(考虑富文本布局),更新你的数组。这样点击行号时,直接用lineNumber作为数组索引就能拿到对应内容,完全解决你的核心问题。
二、SwiftUI TextEditor 实现方案
SwiftUI的TextEditor是UITextView的封装,所以我们需要通过UIViewRepresentable桥接到底层的UITextView,复用上面的逻辑。
代码示例:
// 自定义可检测行点击的TextEditor struct LineDetectableTextEditor: UIViewRepresentable { @Binding var attributedText: AttributedString // 行点击回调:传递行号(从1开始)和该行内容 var onLineTap: (Int, String) -> Void func makeUIView(context: Context) -> UITextView { let textView = UITextView() textView.delegate = context.coordinator textView.isScrollEnabled = true return textView } func updateUIView(_ uiView: UITextView, context: Context) { uiView.attributedText = NSAttributedString(attributedText) } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } // 代理协调器 class Coordinator: NSObject, UITextViewDelegate { let parent: LineDetectableTextEditor init(parent: LineDetectableTextEditor) { self.parent = parent } func textViewDidChangeSelection(_ textView: UITextView) { // 复用UITextView的行号计算逻辑 guard let selectedRange = textView.selectedTextRange, let startPos = selectedRange.start as? UITextPosition else { return } let charOffset = textView.offset(from: textView.beginningOfDocument, to: startPos) let layoutManager = textView.layoutManager let textContainer = textView.textContainer let glyphIndex = layoutManager.glyphIndexForCharacter(at: charOffset) let lineRect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil) let lineNumber = layoutManager.numberOfLines(for: textContainer, upTo: lineRect.origin.y) let lineCharRange = layoutManager.characterRange(forGlyphRange: layoutManager.glyphRange(for: lineRect), actualGlyphRange: nil) let lineText = (textView.attributedText.string as NSString).substring(with: lineCharRange) // 回调给SwiftUI层 parent.onLineTap(lineNumber + 1, lineText) } // 同步文本变化到SwiftUI的绑定值 func textViewDidChange(_ textView: UITextView) { parent.attributedText = AttributedString(textView.attributedText) } } } // 使用示例 struct ContentView: View { @State private var editorText: AttributedString = { var attrString = AttributedString("SwiftUI第一行\nSwiftUI第二行(加粗)\nSwiftUI第三行(斜体)") attrString[attrString.range(of: "SwiftUI第二行(加粗)")!].font = .boldSystemFont(ofSize: 17) attrString[attrString.range(of: "SwiftUI第三行(斜体)")!].font = .italicSystemFont(ofSize: 14) return attrString }() var body: some View { VStack { Text("点击文本任意行查看行号") .padding() LineDetectableTextEditor(attributedText: $editorText) { lineNum, lineContent in print("SwiftUI中点击了第 \(lineNum) 行,内容:\(lineContent)") } .padding() .border(Color.gray) } .padding() } }
关键注意事项
- 富文本兼容性:用
layoutManager计算行号的核心优势是,它会考虑富文本的行高、行间距、字体大小等因素,不会因为样式不同导致行号计算错误。 - 行号起始值:
layoutManager.numberOfLines返回的是从0开始的索引,如果你习惯从1开始计数,记得加1。 - 数组同步:一定要在文本变化时更新你的行数组,避免数组内容和实际文本不一致,上面的代码已经处理了这个逻辑。
内容的提问来源于stack exchange,提问作者Sami Almalki
相关产品推荐
相关产品推荐

