【问题标题】:How to know which line of a TextView the user has tapped on如何知道用户点击了 TextView 的哪一行
【发布时间】:2020-11-29 06:55:06
【问题描述】:

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

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

我正在考虑将整个文本存储在一个数组中(每行是一个元素),并不断更新数组,但问题仍然存在,我该如何知道哪一行被点击来相应地更新数组?

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

【问题讨论】:

  • 这并不容易。您肯定需要使用selectedRange 方法。
  • 能否请您发表一个详细的答案?

标签: ios swift macos swiftui uitextview


【解决方案1】:

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
}()

这是结果:

【讨论】:

  • OP 要求使用 SwiftUI,
  • @zeytin 你是对的! Ooooooooo escusse me,我读得不好……祝你有美好的一天
  • 你的代码很整洁,即使它是 Swift 的,祝你有美好的一天
  • 我认为这个问题是关于在他们点击文本时获取行号。得到他们点击的消息后你会去哪里?
  • @Fabio 你是最棒的!非常感谢:)
【解决方案2】:

我尚未对此进行全面测试,但您可以这样做:选择字符范围后,您可以获得生成的字形范围,如下所示:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-22
    • 2012-04-18
    • 1970-01-01
    相关资源
    最近更新 更多