问题
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