Search code examples
iosswifttextviewgesturecgpoint

Swift - Textview Identify Tapped Word Not Working


Long time user, first time poster, so my apologies if I make any errors in presenting my question. I have been working on this for hours and I've decided it is time to ask the experts. I have also searched through every similar question that has been "answered" and work, which leads me to believe they are outdated.

I am attempting to grab the tapped word from a UITextview that would be used later in the code. For example, there is a paragraph of words in the text view:

"The initial return on time investment is much smaller, due to him trading his upfront cost for sweat-equity in the company, but the potential long-term payout is much greater".

I would want to be able to tap on a word, e.g. 'investment', and run it through another function to define it. However simply tapping the word, crashes the program, and I do not receive the word tapped.

I implemented a tap gesture recognizer:

let tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(_:)))
    tap.delegate = self
    tvEditor.addGestureRecognizer(tap)

and then wrote the function: 2

func tapResponse(recognizer: UITapGestureRecognizer) {
    let location: CGPoint = recognizer.locationInView(tvEditor)
    let position: CGPoint = CGPointMake(location.x, location.y)
    let tapPosition: UITextPosition = tvEditor.closestPositionToPoint(position)!
    let textRange: UITextRange = tvEditor.tokenizer.rangeEnclosingPosition(tapPosition, withGranularity: UITextGranularity.Word, inDirection: 1)!

    let tappedWord: String = tvEditor.textInRange(textRange)!
    print("tapped word : %@", tappedWord)
}

Ideally, this should take the location from the tapped part of the Textview, take the position by taking the .x & .y, and then looking through the Textview at the point closest to the position, finding the Range enclosing the position with granularity (to return the word), and setting the contents as a String, which I am currently just printing to the console. However, on tapping the word, I receive this crash.3

along with "fatal error: unexpectedly found nil while unwrapping an Optional value" in the console.

Any help would be greatly appreciated. I may just be missing something simple, or it could be much more complicated.


Solution

  • 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)")
            }
    
        }
    }