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:
Detecting Tapped String in UITextView
Added UITapGestureRecognizer
to the UITextView
and below is the UITapGestureRecognizer
Selector 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
Why Is the Tab Bar Disappearing
Swift - Json Error: the Data Couldn'T Be Read Because It Isn'T in the Correct Format
Redirect to Application If Installed, Otherwise to App Store
Presenting Modal in iOS 13 Fullscreen
How to Force Nslocalizedstring to Use a Specific Language
Determine Device (Iphone, Ipod Touch) With Ios
React-Native, How to Get File-Asset Image Absolute Path
Ios: How to Make a Shadow for Uiview on 4 Side (Top,Right,Bottom and Left)
Download Array of Images as Byte Array and Convert to Images
How to Programmatically Scroll Through a Collection View
Passing Data Between View Controllers
Using Auto Layout in Uitableview For Dynamic Cell Layouts & Variable Row Heights
How to Prevent Tableview Section Head from Sticking While Scrolling
How to Clear the Entered Textfield When the User Taps on the Button
First and Last Day of the Current Month in Swift
The Best Way to Remove Duplicate Values from Nsmutablearray in Objective-C