Get tapped word from UITextView in Swift
You need to add the UITapGestureRecognizer
to the UITextView
that you want to be able to tap. You are presently adding the UITapGestureRecognizer
to your ViewController
's view
. That is why the cast is getting you into trouble. You are trying to cast a UIView
to a UITextView
.
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textTapped))
tapGesture.numberOfTapsRequired = 1
myTextView.addGestureRecognizer(tapGesture)
Technically recognizer.view
is an optional type (UIView!
) and could be nil
, but it seems unlikely that your textTapped()
would be called it that wasn't set. Likewise, the layoutManager
is of type NSLayoutManager!
. To be on the safe side though, the Swift way to do this is:
guard let textView = recognizer.view as? UITextView, let layoutManager = textView.layoutManager else {
return
}
// code using textView and layoutManager goes here
In fact, if you had written it this way, you wouldn't have crashed because the conditional cast of the UIView
to UITextView
would not have succeeded.
To make this all work then, add attributes to your attributed string that you will extract in your textTapped routine:
var beginning = NSMutableAttributedString(string: "To the north you see a ")
var attrs = [NSFontAttributeName: UIFont.systemFontOfSize(19.0), "idnum": "1", "desc": "old building"]
var condemned = NSMutableAttributedString(string: "condemned building", attributes: attrs)
beginning.appendAttributedString(condemned)
attrs = [NSFontAttributeName: UIFont.systemFontOfSize(19.0), "idnum": "2", "desc": "lake"]
var lake = NSMutableAttributedString(string: " on a small lake", attributes: attrs)
beginning.appendAttributedString(lake)
myTextView.attributedText = beginning
Here's the full textTapped
:
@objc func textTapped(recognizer: UITapGestureRecognizer) {
guard let textView = recognizer.view as? UITextView, let layoutManager = textView.layoutManager else {
return
}
var location: CGPoint = recognizer.locationInView(textView)
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
/*
Here is what the Documentation looks like :
Returns the index of the character falling under the given point,
expressed in the given container's coordinate system.
If no character is under the point, the nearest character is returned,
where nearest is defined according to the requirements of selection by touch or mouse.
This is not simply equivalent to taking the result of the corresponding
glyph index method and converting it to a character index, because in some
cases a single glyph represents more than one selectable character, for example an fi ligature glyph.
In that case, there will be an insertion point within the glyph,
and this method will return one character or the other, depending on whether the specified
point lies to the left or the right of that insertion point.
In general, this method will return only character indexes for which there
is an insertion point (see next method). The partial fraction is a fraction of the distance
from the insertion point logically before the given character to the next one,
which may be either to the right or to the left depending on directionality.
*/
var charIndex = layoutManager.characterIndexForPoint(location, inTextContainer: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard charIndex < textView.textStorage.length else {
return
}
var range = NSRange(location: 0, length: 0)
if let idval = textView.attributedText?.attribute("idnum", atIndex: charIndex, effectiveRange: &range) as? NSString {
print("id value: \(idval)")
print("charIndex: \(charIndex)")
print("range.location = \(range.location)")
print("range.length = \(range.length)")
let tappedPhrase = (textView.attributedText.string as NSString).substringWithRange(range)
print("tapped phrase: \(tappedPhrase)")
var mutableText = textView.attributedText.mutableCopy() as NSMutableAttributedString
mutableText.addAttributes([NSForegroundColorAttributeName: UIColor.redColor()], range: range)
textView.attributedText = mutableText
}
if let desc = textView.attributedText?.attribute("desc", atIndex: charIndex, effectiveRange: &range) as? NSString {
print("desc: \(desc)")
}
}
Swift - Textview Identify Tapped Word Not Working
Swift 3.0 Answer - Working as of July 1st, 2016
In my ViewDidLoad() -
I use text from a previous VC, so my variable "theText" is already declared. I included a sample string that has been noted out.
//Create a variable of the text you wish to attribute.
let textToAttribute = theText // or "This is sample text"
// Break your string in to an array, to loop through it.
let textToAttributeArray = textToAttribute.componentsSeparatedByString(" ")
// Define a variable as an NSMutableAttributedString() so you can append to it in your loop.
let attributedText = NSMutableAttributedString()
// Create a For - In loop that goes through each word your wish to attribute.
for word in textToAttributeArray{
// Create a pending attribution variable. Add a space for linking back together so that it doesn't looklikethis.
let attributePending = NSMutableAttributedString(string: word + " ")
// Set an attribute on part of the string, with a length of the word.
let myRange = NSRange(location: 0, length: word.characters.count)
// Create a custom attribute to get the value of the word tapped
let myCustomAttribute = [ "Tapped Word:": word]
// Add the attribute to your pending attribute variable
attributePending.addAttributes(myCustomAttribute, range: myRange)
print(word)
print(attributePending)
//append 'attributePending' to your attributedText variable.
attributedText.appendAttributedString(attributePending) ///////
print(attributedText)
}
textView.attributedText = attributedText // Add your attributed text to textview.
Now we will add a tap gesture recognizer to register taps.
let tap = UITapGestureRecognizer(target: self, action: #selector(HandleTap(_:)))
tap.delegate = self
textView.addGestureRecognizer(tap) // add gesture recognizer to text view.
Now we declare a function under the viewDidLoad()
func HandleTap(sender: UITapGestureRecognizer) {
let myTextView = sender.view as! UITextView //sender is TextView
let layoutManager = myTextView.layoutManager //Set layout manager
// location of tap in myTextView coordinates
var location = sender.locationInView(myTextView)
location.x -= myTextView.textContainerInset.left;
location.y -= myTextView.textContainerInset.top;
// character index at tap location
let characterIndex = layoutManager.characterIndexForPoint(location, inTextContainer: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if index is valid then do something.
if characterIndex < myTextView.textStorage.length {
// print the character index
print("Your character is at index: \(characterIndex)") //optional character index.
// print the character at the index
let myRange = NSRange(location: characterIndex, length: 1)
let substring = (myTextView.attributedText.string as NSString).substringWithRange(myRange)
print("character at index: \(substring)")
// check if the tap location has a certain attribute
let attributeName = "Tapped Word:" //make sure this matches the name in viewDidLoad()
let attributeValue = myTextView.attributedText.attribute(attributeName, atIndex: characterIndex, effectiveRange: nil) as? String
if let value = attributeValue {
print("You tapped on \(attributeName) and the value is: \(value)")
}
}
}
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);
}
Get word from character index in textView (iOS)
The issue apparently seemed to stem from the .x and .y values I set after getting location. All I had to do was remove these lines:
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
Get word from long tap in a word of UITextView
This function will return the word at a given position in an UITextView.
+(NSString*)getWordAtPosition:(CGPoint)pos inTextView:(UITextView*)_tv
{
//eliminate scroll offset
pos.y += _tv.contentOffset.y;
//get location in text from textposition at point
UITextPosition *tapPos = [_tv closestPositionToPoint:pos];
//fetch the word at this position (or nil, if not available)
UITextRange * wr = [_tv.tokenizer rangeEnclosingPosition:tapPos withGranularity:UITextGranularityWord inDirection:UITextLayoutDirectionRight];
return [_tv textInRange:wr];
}
is there any chance to identify if empty space has been tapped in UITextView?
Ok, so I got it working like expected. The solution is to check if the tapped position is the end of the document:
if let position = textView.closestPosition(to: location) {
if position == textView.endOfDocument {
return true
}
}
The final code looks like that:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if let textView = textView, textView.text.count > 0 {
var location = touch.location(in: textView)
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
if let position = textView.closestPosition(to: location) {
if position == textView.endOfDocument {
return true
}
}
let characterIndex = textView.layoutManager.characterIndex(for: location, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if (textView.attributedText?.attribute(.link, at: characterIndex, effectiveRange: nil) as? URL) != nil {
return false
}
}
return true
}
Related Topics
Always Stop in App Delegate After Enabling All Exceptions Break Point
How to Activate Tcp Keepalive on Apple iOS Devices
Xcode 6.3.2 Can't Run on Osx 10.11 El Capitan Developer Preview 3
How to Create a Bold Uifont from a Regular Uifont
Apple MACh-O Linker & Ditto Error - Xcode 8
Move Gmsmarker on Google Map Like Uber
Property "Assign" and "Retain" for Delegate
How to Set Mdm Payload's "Identity" in Ipcu
Ios: Rounded Rectangle with Border Bleeds Color
Swift: Equivalent Objective-C Runtime Class
How to Upload Video to Server from Iphone
iOS Input Focused Inside Fixed Parent Stops Position Update of Fixed Elements
How to Use Uiactivityitemprovider to Send an Email with Attachment with Uiactivityviewcontroller