Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get tapped word from UITextView in Swift

I know that this problem has been solved in Objective-C, but I haven't seen any solution to it in Swift. I have tried to convert the solution code from this post, but I'm getting errors:

func textTapped(recognizer: UITapGestureRecognizer){

    var textView: UITextView = recognizer.view as UITextView
    var layoutManager: NSLayoutManager = textView.layoutManager
    var location: CGPoint = recognizer.locationInView(textView)
    location.x -= textView.textContainerInset.left
    location.y -= textView.textContainerInset.top

    var charIndex: Int
    charIndex = layoutManager.characterIndexForPoint(location, inTextContainer: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

    if charIndex < textView.textStorage.length {
        // do the stuff
        println(charIndex)
    }
}

I think the problem is in this line (see error here):

 var textView: UITextView = recognizer.view as UITextView

... which I have converted from Objective-C based on this line:

 UITextView *textView = (UITextView *)recognizer.view;

Finally, I am also in doubt about how this function should be called. As I understand it, the function should be passed to a Selector in viewDidLoad(), like this:

 let aSelector: Selector = "textTapped:"   

 let tapGesture = UITapGestureRecognizer(target: self, action: aSelector)
 tapGesture.numberOfTapsRequired = 1
 view.addGestureRecognizer(tapGesture)

Because I'm getting the before mentioned error, I'm not sure if it would work. But I'm thinking that I would need to pass the parameter in the textTapped function (recognizer) into the Selector as well. However, I've read that you can only pass the function and not any parameters.

like image 784
Benjamin Hviid Avatar asked Aug 23 '14 18:08

Benjamin Hviid


2 Answers

For Swift 3.0 OR Above

add Tap gesture to UITextView

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOnTextView(_:)))
textView.addGestureRecognizer(tapGesture)

add tap handler method

@objc private final func tapOnTextView(_ tapGesture: UITapGestureRecognizer){

  let point = tapGesture.location(in: textView)
  if let detectedWord = getWordAtPosition(point)
  {

  }
}

get word from point

private final func getWordAtPosition(_ point: CGPoint) -> String?{
if let textPosition = textView.closestPosition(to: point)
{
  if let range = textView.tokenizer.rangeEnclosingPosition(textPosition, with: .word, inDirection: 1)
  {
    return textView.text(in: range)
  }
}
return nil}
like image 67
Hiren Panchal Avatar answered Oct 06 '22 09:10

Hiren Panchal


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)")
    }
}
like image 38
vacawama Avatar answered Oct 06 '22 08:10

vacawama