3

I want to get the currently typed word in a UITextView. A way to get a completely typed word can be found here UITEXTVIEW: Get the recent word typed in uitextview, however, I want to get the word while it is being typed (no matter where it is typed, beginning, middle, end of UITextView).

I guess what defines a word being typed is a whitespace character followed by alphanumeric characters, or if the current location (somehow to figure out with range.location) is the beginning of the UITextView then whatever follows up should be considered as word as well. A word is finished being typed when another whitespace character follows.

I tried with:

 - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text`

but I have a problem finding the correct range.

In short again: I need to find the substring in a UITextView, where substring is the currently typed word.

EDIT: Because the question came up. I'm using NSLayoutManager and bind a NSTextContainer to it, which then is passed as layoutManager to a NSTextStorage.

EDIT2: The main problem with (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text is that the range.location is not the same as the indicator is, but always 1 less. If the cursor is at position 1, after typing one letter, range.location would return 0. Any way around this? In addition the text attribute of UITextView seems to be off by 1 as well. When the text is foobar and I output the UITextView.text then I get fooba instead. Therefore range.location+1 returns the exception Range {0, 1} out of bounds; string length 0'

EDIT3: Maybe it is easier to get my wanted result with the NSTextStorage instead of UITextView?

Community
  • 1
  • 1
user3607973
  • 345
  • 1
  • 5
  • 15
  • 2
    `- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text` method will give you range for single character at a time and you have to take it one by one and make latest word – Mehul Thakkar Dec 09 '14 at 13:13
  • Where are you wanting to display it? In a label or? That will help me – App Dev Guy Dec 09 '14 at 13:33
  • You should look into `UITextInputProtocol`. If you face any issues implementing code, let me know. – n00bProgrammer Dec 09 '14 at 13:55
  • @The-Rooster It's a little 'complicated' setup. I'm using `NSLayoutManager`and bind a `NSTextContainer` to it, which then is passed as `layoutManager` to a `NSTextStorage`. – user3607973 Dec 09 '14 at 17:07
  • @n00bProgrammer I just took a look at `UITextInputProtocol` and don't really know how it is suppose to help. Any specifics in the `UITextInputProtocol` where I should take a closer look at? – user3607973 Dec 09 '14 at 18:34
  • Checkout my solution https://stackoverflow.com/a/46828561/2020034 – Vinayak Parmar Oct 19 '17 at 11:10

5 Answers5

10

The following is an example of a skeletal structure that uses UITextInput and UITextInputTokenizer to give the word that is being currently typed/edited.

- (void) textViewDidChange:(UITextView *)textView {

    NSRange selectedRange = textView.selectedRange;

    UITextPosition *beginning = textView.beginningOfDocument;
    UITextPosition *start = [textView positionFromPosition:beginning offset:selectedRange.location];
    UITextPosition *end = [textView positionFromPosition:start offset:selectedRange.length];

    UITextRange* textRange = [textView.tokenizer rangeEnclosingPosition:end withGranularity:UITextGranularityWord inDirection:UITextLayoutDirectionLeft];

    NSLog(@"Word that is currently being edited is : %@", [textView textInRange:textRange]);
}

This will give you the entire word that is currently being typed.

For instance if you are typing input and have typed inp it will give you inp. Further if you are in the middle of the word middle and change it to midddle, it will give you midddle. The trick here is tokenizing.

I have placed the code inside textViewDidChange: so that you get the word after it's been typed/edited. If you want it before the last character change (i.e. the word that is about to be changed), place the code in textView:shouldChangeTextInRange:replacementText:.

PS: You will have to handle edge cases like sentences and paragraphs being pasted/removed. You can modify this code to get characters/sentences/paragraphs etc, instead of just words by changing the granularity. Look into the declaration of UITextGranularity ENUM for more options:

typedef NS_ENUM(NSInteger, UITextGranularity) {
    UITextGranularityCharacter,
    UITextGranularityWord,
    UITextGranularitySentence,
    UITextGranularityParagraph,
    UITextGranularityLine,
    UITextGranularityDocument
};
n00bProgrammer
  • 4,261
  • 3
  • 32
  • 60
  • Great, but it does not work with a _UITextGranularityWord_ and special characters : ex: "blabla **#myCurrentlyEditedWord** blabla", that's return "myCurrentlyEditedWord" without the **"#"**. – Ded77 Jan 13 '16 at 17:32
  • @Ded77 you're absolutely right. That's because granularity works with languages (I think, but I'm not sure). **#**, here is a special character. As mentioned in the **PS** part of the answer, you will have to handle edge cases, and this is an edge case. One thing you can do is keep a reference to the range of the last word detected and compare its **location+range** with the **location** of the current word detected. If they're the same, that means it's one word (hashtag, handle, etc). Easier way would be to extract the detected word from the main string and check if it is preceded by **#/@**. – n00bProgrammer Jan 13 '16 at 18:06
  • 2
    This is good, but it doesn't work for symbols too bad, because this was almost perfect. With that said, I used this and it seems to work fine, capturing special characters `let range = tokenizer.rangeEnclosingPosition(end, with: .sentence, inDirection: .layout(.left))` and `let sentence = textView.text(in: range)` and `let currentWord = sentence.sentence.components(separatedBy: .whitespaces).last` – nodebase Feb 01 '21 at 19:13
2
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
    NSString *string = [textView.text stringByReplacingCharactersInRange:range withString:text];

    if ([text isEqualToString:@"\n"] || [text isEqualToString:@" "])
    {
        NSString *currentString = [string stringByReplacingOccurrencesOfString:@"\n" withString:@" "];

        NSLog(@"currentWord==> %@",currentString);
    }

    return YES;
}
App Dev Guy
  • 5,396
  • 4
  • 31
  • 54
Ashokios
  • 27
  • 5
  • I already had this solution. The problem however is that you get the WHOLE string back AFTER a whitespace or linebreak character is detected. You don't get back the currently typed word, but the whole string in the `NSTextContainer`. – user3607973 Dec 09 '14 at 17:15
2

You can simply extend UITextView and use following method which returns the word around the current location of cursor:

extension UITextView {

    func editedWord() -> String {

        let cursorPosition = selectedRange.location
        let separationCharacters = NSCharacterSet(charactersInString: " ")

        // Count how many actual characters there are before the cursor.
        // Emojis/special characters can each increase selectedRange.location
        // by 2 instead of 1

        var unitCount = 0
        var characters = 0
        while unitCount < cursorPosition {

            let char = text.startIndex.advancedBy(characters)
            let int = text.rangeOfComposedCharacterSequenceAtIndex(char)
            unitCount = Int(String(int.endIndex))!
            characters += 1
        }


        let beginRange = Range(start: text.startIndex.advancedBy(0), end: text.startIndex.advancedBy(characters))
        let endRange = Range(start: text.startIndex.advancedBy(characters), end: text.startIndex.advancedBy(text.characters.count))

        let beginPhrase = text.substringWithRange(beginRange)
        let endPhrase = text.substringWithRange(endRange)

        let beginWords = beginPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
        let endWords = endPhrase.componentsSeparatedByCharactersInSet(separationCharacters)

        return beginWords.last! + endWords.first!
    }
}
Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
1

The UITextView delegate method : -textView:textView shouldChangeTextInRange:range replacementText:text what actaully does is that it asks whether the specified text should be replaced in the text view in the specified range of textView.text .

This method will be invoked each time when we type a charactor before updating that to the text view. That is why you are getting the range.location as 0, when you type the very first character in the textView.

Only if the return of this method is true, the textView is getting updated with what we have typed in the textView.

This is the definition of the parameters of the -textView:textView shouldChangeTextInRange:range replacementText:text method as provided by apple:

range :- The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character.
text :- The text to insert.

So this is what the explanation for the method and your requirement can meet like as follows:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
        //Un-commend this check below :If you want to detect the word only while new line or white space charactor input 
        //if ([text isEqualToString:@" "] || [text isEqualToString:@"\n"])
        //{
            // Getting the textView text upto the current editing location
            NSString * stringToRange = [textView.text substringWithRange:NSMakeRange(0,range.location)];

            // Appending the currently typed charactor
            stringToRange = [stringToRange stringByAppendingString:text];

            // Processing the last typed word 
            NSArray *wordArray       = [stringToRange componentsSeparatedByString:@" "];
            NSString * wordTyped     = [wordArray lastObject];

            // wordTyped will give you the last typed object
            NSLog(@"\nWordTyped :  %@",wordTyped);
        //}
        return YES;
}
Alex Andrews
  • 1,498
  • 2
  • 19
  • 33
  • I've seen this solution before, but unfortunately it doesn't quite work. When you output `wordTyped`, the word you get is always one character short. So, lets say you just typed `foobar`, `NSLog` will output you `fooba` for the `wordTyped`. – user3607973 Dec 09 '14 at 18:30
  • That's what I thought at the beginning too, but that doesn't work at all and also crashes the app when the `UITextView` was empty before and gives an out of bounds exception – user3607973 Dec 09 '14 at 18:47
  • That doesn't work, because it will only output the `wordTyped`, if a whitespace character or linebreak is received, but I want to get the output all the time. – user3607973 Dec 09 '14 at 19:53
  • The problem is described in this question http://stackoverflow.com/questions/9155381/autocompletion-using-uitextview-should-change-text-in-range. The idea is to have the replacement before. – user3607973 Dec 10 '14 at 01:17
0

Your range parameter from the - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text delegate method points at the exact place you change the UITextView string value.

The location property of the structure points to the changing string index, that's simple. The length not usually equals to text.length, be careful, user may select characters from the string to replace it with another string.

Daniyar
  • 2,975
  • 2
  • 26
  • 39