Get Tapped Word from Uitextview in Swift

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:

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

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



Leave a reply



Submit