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

杀马特。学长 韩版系。学妹 提交于 2021-01-28 07:09:31

问题


I'm building an app that has a multi-line text editor (UITextView in UIKit, and TextEditor in SwiftUI). When the user tap on any line of the multi-line text, I should know which line is that, so I can print that line or do any other task.

Is there a way to know which line of a UITextView the user has tapped on (using textViewDidChangeSelection delegate function or anything else)?

I was thinking of storing the whole text in an Array (each line is an element), and updating the array continuously, but the problem still exists, how shall I know which line has been tapped on to update the array accordingly?

Note: I'm using Attributed Strings to give every line different styling.


回答1:


Swift 5 UIKit Solution:

Try to add tap gesture to textView and detect the word tapped:

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

After that in viewDidLoad set tap gesture and constraints:

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

now call the function to detect word:

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

with Attributed Strings it is the same thing.

UPDATE

Add this function to detect line:

@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)")
        }
    }
}

don't forget to change called function on tap:

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

EDIT TO SHOW CURSOR AND SET IT POSITION ON TAP

to show the cursor on tap location add ended state to recognizer in didTapTextView function set text view is editable and become first responder, this is your didTapTextView function look like:

@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)")
        }
    }
}

in my example I set cursor color to green to make it much visible, to do it set textView tint color (I added on TextView attributed text):

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

This is te result:




回答2:


I haven't tested out this to the full extent, but here is what you can do: having the selected character range, you can get the generated glyph range like so:

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

Now you have a glyph range, and can use textView.layoutManager.enumerateLineFragments method to find the line fragment (displayed in text view), corresponding to your glyph range. After this, all that remains is to convert the found glyph range for line fragment to character range using NSLayoutManager's method characterRange(forGlyphRange, actualGlyphRange:nil)

This seems to do the trick, although you might need to make adjustments to fit this to your situation (I've tested this on plain text w/o any attributes applied).



来源:https://stackoverflow.com/questions/65057755/how-to-know-which-line-of-a-textview-the-user-has-tapped-on

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!