How to Get the Index (String.Index) Value from the Cursor Position of a Uitextview Element in Swift

is it possible to get the index (String.Index) value from the cursor position of a UITextView element in Swift?

You can get the selected range offset from the beginning of your document to the start of the selected range and use that offset to get your string index as follow:

extension UITextView {
var cursorOffset: Int? {
guard let range = selectedTextRange else { return nil }
return offset(from: beginningOfDocument, to: range.start)
}
var cursorIndex: String.Index? {
guard let location = cursorOffset else { return nil }
return Range(.init(location: location, length: 0), in: text)?.lowerBound
}
var cursorDistance: Int? {
guard let cursorIndex = cursorIndex else { return nil }
return text.distance(from: text.startIndex, to: cursorIndex)
}
}


class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
}
func textViewDidChangeSelection(_ textView: UITextView) {
print("Cursor UTF16 Offset:",textView.cursorOffset ?? "")
print("Cursor Index:", textView.cursorIndex ?? "")
print("Cursor distance:", textView.cursorDistance ?? "")
}
}

swift cursor position with emojis

Your issue is that the NSRange value returned by UITextField selectedTextRange and the offset need to be properly converted to a Swift String.Index.

func getCursorPosition(_ textField: UITextField) -> String.Index? {
if let selectedRange = textField.selectedTextRange {
let cursorPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.end)
let positionRange = NSRange(location: 0, length: cursorPosition)
let stringOffset = Range(positionRange, in: textField.text!)!

return stringOffset.upperBound
}

return nil
}

Once you have that String.Index you can split the string.

if let index = getCursorPosition(textField) {
let textBeforeCursor = textField.text![..<index]
let textAfterCursor = textField.text![index...]
}

How can we get the character before cursor?

You can simply get the text from the beginning of the document up to the start of the cursor range and get the last character:

extension UITextView {
var characterBeforeCaret: Character? {
if let selectedTextRange = self.selectedTextRange,
let range = textRange(from: beginningOfDocument, to: selectedTextRange.start) {
return text(in: range)?.last
}
return nil
}
}

If you would like to get the character before the caret without getting the whole text before it you will need to convert the UITextPosition to a String.Index as you can see in my post here offset the index by minus one character and return the character at that index:

extension UITextView {
var characterBeforeCaret: Character? {
if let textRange = self.selectedTextRange,
let start = Range(.init(location: offset(from: beginningOfDocument, to: textRange.start), length: 0), in: text)?.lowerBound,
let index = text.index(start, offsetBy: -1, limitedBy: text.startIndex) {
return text[index]
}
return nil
}
}

Getting and Setting Cursor Position of UITextField and UITextView in Swift

The following content applies to both UITextField and UITextView.

Useful information

The very beginning of the text field text:

let startPosition: UITextPosition = textField.beginningOfDocument

The very end of the text field text:

let endPosition: UITextPosition = textField.endOfDocument

The currently selected range:

let selectedRange: UITextRange? = textField.selectedTextRange

Get cursor position

if let selectedRange = textField.selectedTextRange {

let cursorPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)

print("\(cursorPosition)")
}

Set cursor position

In order to set the position, all of these methods are actually setting a range with the same start and end values.

To the beginning

let newPosition = textField.beginningOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)

To the end

let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)

To one position to the left of the current cursor position

// only if there is a currently selected range
if let selectedRange = textField.selectedTextRange {

// and only if the new position is valid
if let newPosition = textField.position(from: selectedRange.start, offset: -1) {

// set the new position
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}

To an arbitrary position

Start at the beginning and move 5 characters to the right.

let arbitraryValue: Int = 5
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: arbitraryValue) {

textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}

Related

Select all text

textField.selectedTextRange = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument)

Select a range of text

// Range: 3 to 7
let startPosition = textField.position(from: textField.beginningOfDocument, offset: 3)
let endPosition = textField.position(from: textField.beginningOfDocument, offset: 7)

if startPosition != nil && endPosition != nil {
textField.selectedTextRange = textField.textRange(from: startPosition!, to: endPosition!)
}

Insert text at the current cursor position

textField.insertText("Hello")

Notes

  • Use textField.becomeFirstResponder() to give focus to the text field and make the keyboard appear.

  • See this answer for how to get the text at some range.

See also

  • How to Create a Range in Swift

How to get the Character that is before the cursor in a UITextView?

Here's an idea, not fully tested, but seems to work... Just grab the character about to be acted upon and block backspace if its the target... Also with regard to selection of text, if the selection contains the target at all, we block new text.

import UIKit

class ViewController: UIViewController, UITextViewDelegate {

@IBOutlet weak var textView: UITextView!

override func viewDidLoad() {
super.viewDidLoad()
self.textView.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}

func characterBeforeCursor() -> String? {
// get the cursor position
if let cursorRange = textView.selectedTextRange {
// get the position one character before the cursor start position
if let newPosition = textView.position(from: cursorRange.start, offset: -1) {
let range = textView.textRange(from: newPosition, to: cursorRange.start)
return textView.text(in: range!)
}
}
return nil
}

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (characterBeforeCursor() == "W") {
let char = text.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
return false
}
return true
}
else {
if let range = textView.selectedTextRange {
let selectedText = textView.text(in: range)
if (selectedText!.contains("W")) {
return false
}
}
return true
}
}
}

Get currently typed word in a UITextView

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;
}

Detecting Cursor position in UITextView that contains emojis returns the wrong position in swift 4

Long story short: When using Swift with String and NSRange use this extension for Range conversion

extension String {
/// Fixes the problem with `NSRange` to `Range` conversion
var range: NSRange {
let fromIndex = unicodeScalars.index(unicodeScalars.startIndex, offsetBy: 0)
let toIndex = unicodeScalars.index(fromIndex, offsetBy: count)
return NSRange(fromIndex..<toIndex, in: self)
}
}

Let's take a deeper look:

let myStr = "Wéll helló ⚙️"
myStr.count // 12
myStr.unicodeScalars.count // 13
myStr.utf8.count // 19
myStr.utf16.count // 13

In Swift 4 string is a collection of characters (composite character like ö and emoji will count as one character). UTF-8 and UTF-16 views are the collections of UTF-8 and UTF-16 code units respectively.

Your problem is, that textView.text.count counts collection elements (emoji as well as composite character will count as one element) and NSRange counts indexes of UTF-16 code units. The difference is illustrated in the snipped above.


More here:
Strings And Characters



Related Topics



Leave a reply



Submit