Detecting Taps on Attributed Text in a Uitextview in Ios

Detecting taps on attributed text in a UITextView in iOS

I just wanted to help others a little more. Following on from Shmidt's response it's possible to do exactly as I had asked in my original question.

1) Create an attributed string with custom attributes applied to the clickable words. eg.

NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a clickable word" attributes:@{ @"myCustomTag" : @(YES) }];
[paragraph appendAttributedString:attributedString];

2) Create a UITextView to display that string, and add a UITapGestureRecognizer to it. Then handle the tap:

- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
UITextView *textView = (UITextView *)recognizer.view;

// Location of the tap in text-container coordinates

NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [recognizer locationInView:textView];
location.x -= textView.textContainerInset.left;
location.y -= textView.textContainerInset.top;

// Find the character that's been tapped on

NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];

if (characterIndex < textView.textStorage.length) {

NSRange range;
id value = [textView.attributedText attribute:@"myCustomTag" atIndex:characterIndex effectiveRange:&range];

// Handle as required...

NSLog(@"%@, %d, %d", value, range.location, range.length);

}
}

So easy when you know how!

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

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:

Sample Image

Detecting Tapped String in UITextView

Added UITapGestureRecognizer to the UITextView and below is the UITapGestureRecognizerSelector method.

- (void) tapResponse:(UITapGestureRecognizer *)recognizer{

UITextView *textView = (UITextView *)recognizer.view;
CGPoint location = [recognizer locationInView:textView];
NSLog(@"Tap Gesture Coordinates: %.2f %.2f -- %@", location.x, location.y,textView.text);

CGPoint position = CGPointMake(location.x, location.y);
//get location in text from textposition at point
UITextPosition *tapPosition = [textView closestPositionToPoint:position];

//fetch the word at this position (or nil, if not available)
UITextRange *textRange = [textView.tokenizer rangeEnclosingPosition:tapPosition withGranularity:UITextGranularityWord inDirection:UITextLayoutDirectionRight];
NSString *tappedSentence = [textView textInRange:textRange];//[self lineAtPosition:CGPointMake(location.x, location.y)];
NSLog(@"selected :%@ -- %@",tappedSentence,tapPosition);
}

UITextView/NSAttribute: Detect if word starts with a particular symbol

Assuming you keep # or @ in the text, you can modify the answer in Larme's comment:

let regex = try! NSRegularExpression(pattern: "(?:#|@)\\w+", options: [])

func textViewDidChange(_ textView: UITextView) {
let attrStr = NSMutableAttributedString(attributedString: textView.attributedText ?? NSAttributedString())
let plainStr = attrStr.string
attrStr.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(0..<plainStr.utf16.count))

let matches = regex.matches(in: plainStr, range: NSRange(0..<plainStr.utf16.count))

for match in matches {
let nsRange = match.range
let matchStr = plainStr[Range(nsRange, in: plainStr)!]
let color: UIColor
if matchStr.hasPrefix("#") {
color = .red
} else {
color = .blue
}
attrStr.addAttribute(.foregroundColor, value: color, range: nsRange)
}

textView.attributedText = attrStr
}

I just have changed the pattern, adapted to Swift 4.1, fixed some bugs, removed some redundant codes and added some code to change colors.



Related Topics



Leave a reply



Submit