Jake Braden
Jake Braden

Reputation: 489

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.

Upvotes: 3

Views: 3556

Answers (3)

deyanm
deyanm

Reputation: 484

In addition to @AmitSingh answer, this is updated Swift 3.0 version:

    func didTapTextView(recognizer: UITapGestureRecognizer) {
    let location: CGPoint = recognizer.location(in: textView)
    let position: CGPoint = CGPoint(x: location.x, y: location.y)
    let tapPosition: UITextPosition? = textView.closestPosition(to: position)
    if tapPosition != nil {
        let textRange: UITextRange? = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: 1)
        if textRange != nil
        {
            let tappedWord: String? = textView.text(in: textRange!)
            print("tapped word : ", tappedWord!)
        }
    }
}

The other code is the same as his.

Hope it helps!

Upvotes: 3

Jake Braden
Jake Braden

Reputation: 489

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

    }
}

Upvotes: 3

Amit Singh
Amit Singh

Reputation: 2698

  • Whenever the tapped received at the blank spaces between the words, tapPosition returned by the TextView can be nil nil.
  • Swift has new operator called optional ? which tells the compiler that the variable may have nil value. If you do not use ? after the variable name indicates that the variable can never have nil value.
  • In Swift, using ! operator means you are forcing the compiler to forcefully extract the value from the optional variable. So, in that case, if the value of the variable is nil, it will crash on forcing.

So, what is actually happening is

  • You are creating the variable let tapPosition: UITextPosition, let textRange: UITextRange and let tappedWord: String are not optional
  • return type of the method myTextView.closestPositionToPoint(position), tvEditor.textInRange(textRange) are optional variable UITextPosition?, String?
  • Assigning a value of optional variable to non optional variable requires !
  • The method is returning nil and you are forcing it to get the value ! lead to CRASH

What you can do

  • Before forcing any optional variable, just be sure that it has some value using

    if variable != nil
    {
        print(variable!)
    }
    

Correct method would be as

func tapResponse(recognizer: UITapGestureRecognizer) {
    let location: CGPoint = recognizer.locationInView(myTextView)
    let position: CGPoint = CGPointMake(location.x, location.y)
    let tapPosition: UITextPosition? = myTextView.closestPositionToPoint(position)
    if tapPosition != nil {
        let textRange: UITextRange? = myTextView.tokenizer.rangeEnclosingPosition(tapPosition!, withGranularity: UITextGranularity.Word, inDirection: 1)
        if textRange != nil
        {
            let tappedWord: String? = myTextView.textInRange(textRange!)
            print("tapped word : ", tappedWord)
        }
    }
}

Upvotes: 3

Related Questions