如何知道用户点击了 TextView 的哪一行

How to know which line of a TextView the user has tapped on

我正在构建一个具有多行文本编辑器(UIKit 中的 UITextView 和 SwiftUI 中的 TextEditor)的应用程序。当用户点击多行文本的任何一行时,我应该知道那是哪一行,这样我就可以打印该行或执行任何其他任务。

有没有办法知道用户点击了 UITextView 的哪一行(使用 textViewDidChangeSelection 委托函数或其他任何方式)?

我想把整个文本存入一个数组(每行一个元素),不断更新数组,问题依旧,怎么知道是点击了哪一行更新了数组相应地?

注意:我正在使用属性字符串为每一行赋予不同的样式。

Swift 5 UIKit 解决方案:

尝试将点击手势添加到 textView 并检测点击的单词:

let textView: UITextView = {
let tv = UITextView()
tv.text = "ladòghjòdfghjdaòghjjahdfghaljdfhgjadhgf ladjhgf dagf adjhgf adgljdgadsjhladjghl dgfjhdjgh jdahgfljhadlghal dkgjafahd fgjdsfgh adh"
tv.textColor = .black
tv.font = .systemFont(ofSize: 16, weight: .semibold)
tv.textContainerInset = UIEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)
tv.translatesAutoresizingMaskIntoConstraints = false
    
return tv
}()

之后在 viewDidLoad 中设置点击手势和约束:

let tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(recognizer:)))
textView.addGestureRecognizer(tap)
    
view.addSubview(textView)
textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true

现在调用函数检测单词:

@objc func tapResponse(recognizer: UITapGestureRecognizer) {
    let location: CGPoint = recognizer.location(in: textView)
    let position: CGPoint = CGPoint(x: location.x, y: location.y)
    guard let position2 = textView.closestPosition(to: position) else { return }
    let tapPosition: UITextPosition = position2
    guard let textRange: UITextRange = textView.tokenizer.rangeEnclosingPosition(tapPosition, with: UITextGranularity.word, inDirection: UITextDirection(rawValue: 1)) else {return}
    
    let tappedWord: String = textView.text(in: textRange) ?? ""
    print("tapped word:", tappedWord)
}

属性字符串是一样的。

更新

添加此功能检测线路:

@objc func didTapTextView(recognizer: UITapGestureRecognizer) {
    if recognizer.state == .recognized {
        let location = recognizer.location(ofTouch: 0, in: textView)

        if location.y >= 0 && location.y <= textView.contentSize.height {
            guard let font = textView.font else {
                return
            }

            let line = Int((location.y - textView.textContainerInset.top) / font.lineHeight) + 1
            print("Line is \(line)")
        }
    }
}

不要忘记在点击时更改调用的函数:

let tap = UITapGestureRecognizer(target: self, action: #selector(didTapTextView(recognizer:)))

编辑以显示光标并在点击时设置它的位置

在点击位置显示光标在 didTapTextView 函数中将结束状态添加到识别器设置文本视图是可编辑的并成为第一响应者,这是你的 didTapTextView 函数看起来像:

@objc func didTapTextView(recognizer: UITapGestureRecognizer) {
    
    if recognizer.state == .ended {
           textView.isEditable = true
           textView.becomeFirstResponder()

           let location = recognizer.location(in: textView)
           if let position = textView.closestPosition(to: location) {
               let uiTextRange = textView.textRange(from: position, to: position)

               if let start = uiTextRange?.start, let end = uiTextRange?.end {
                   let loc = textView.offset(from: textView.beginningOfDocument, to: position)
                   let length = textView.offset(from: start, to: end)

                   textView.selectedRange = NSMakeRange(loc, length)
               }
           }
       }
    
    if recognizer.state == .recognized {
        let location = recognizer.location(ofTouch: 0, in: textView)

        if location.y >= 0 && location.y <= textView.contentSize.height {
            guard let font = textView.font else {
                return
            }

            let line = Int((location.y - textView.textContainerInset.top) / font.lineHeight) + 1
            print("Line is \(line)")
        }
    }
}

在我的示例中,我将光标颜色设置为绿色以使其更明显,为此设置了 textView 色调颜色(我在 TextView 属性文本上添加):

let textView: UITextView = {
    let tv = UITextView()
    
    tv.textContainerInset = UIEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)
    tv.translatesAutoresizingMaskIntoConstraints = false
    tv.tintColor = .green // cursor color
    
    let attributedString = NSMutableAttributedString(string: "ladòghjòdfghjdaòghjjahdfghaljdfhgjadhgf ladjhgf dagf adjhgf adgljdgadsjhladjghl", attributes: [.font: UIFont.systemFont(ofSize: 20, weight: .regular), .foregroundColor: UIColor.red])
    attributedString.append(NSAttributedString(string: " dgfjhdjgh jdahgfljhadlghal dkgjafahd fgjdsfgh adh jsfgjskbfgfs gsfjgbjasfg ajshg kjshafgjhsakhg shf", attributes: [.font: UIFont.systemFont(ofSize: 20, weight: .bold), .foregroundColor: UIColor.black]))
    
    tv.attributedText = attributedString
    
    return tv
}()

这是结果:

我还没有对此进行全面测试,但您可以执行以下操作:选择字符范围后,您可以像这样获得生成的字形范围:

let glyphRange = textView.layoutManager.glyphRange(forCharacterRange: selectedRange, 
actualCharacterRange: nil)

现在你有了一个字形范围,可以使用 textView.layoutManager.enumerateLineFragments 方法找到与你的字形范围相对应的行片段(显示在文本视图中)。在此之后,剩下的就是使用 NSLayoutManager 的方法 characterRange(forGlyphRange, actualGlyphRange:nil)

将找到的行片段的字形范围转换为字符范围

这似乎可以解决问题,尽管您可能需要进行调整以适应您的情况(我已经在纯文本 w/o 应用的任何属性上对此进行了测试)。