Benjamin Hviid Benjamin Hviid - 1 month ago 12
iOS Question

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.

Answer

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: "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:

if let textView = recognizer.view as? UITextView {
    if let layoutManager = textView.layoutManager {
        // 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:

func textTapped(recognizer: UITapGestureRecognizer){
    if let textView = recognizer.view as? UITextView {
        if let layoutManager = textView.layoutManager {
            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)

            if charIndex < textView.textStorage.length {
                var range = NSRange(location: 0, length: 0)
                if let idval = textView.attributedText?.attribute("idnum", atIndex: charIndex, effectiveRange: &range) as? NSString {
                    println("id value: \(idval)")
                    println("charIndex: \(charIndex)")
                    println("range.location = \(range.location)")
                    println("range.length = \(range.length)")
                    let tappedPhrase = (textView.attributedText.string as NSString).substringWithRange(range)
                    println("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 {
                    println("desc: \(desc)")
                }
            }
        }
    }
}