UITextField
UITextView
Swift programming
cursor position
iOS development

Getting and Setting Cursor Position of UITextField and UITextView in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Programmatically reading or moving the text cursor is common in input-heavy iOS screens such as masked forms, chat composers, and editors. UITextField and UITextView both expose the necessary APIs through selectedTextRange, UITextPosition, and offset(from:to:), but the model is document-based rather than simple integer indexing.

How UIKit Represents Cursor Position

UIKit does not expose the caret as a raw integer property. Instead, text controls work with document positions and ranges:

  • 'beginningOfDocument'
  • 'endOfDocument'
  • 'selectedTextRange'
  • 'position(from:offset:)'

For a plain insertion cursor, selectedTextRange.start and selectedTextRange.end are the same position. If the user has selected text, those positions differ.

Reading the Cursor Position in a UITextField

The usual pattern is to measure the offset from beginningOfDocument to the start of the current selection.

swift
1import UIKit
2
3func cursorOffset(in textField: UITextField) -> Int? {
4    guard let selectedRange = textField.selectedTextRange else {
5        return nil
6    }
7
8    return textField.offset(
9        from: textField.beginningOfDocument,
10        to: selectedRange.start
11    )
12}

That gives you a zero-based offset that you can log, store, or reuse after formatting text.

Setting the Cursor Position in a UITextField

To move the insertion point, convert the target offset back into a UITextPosition, then build a zero-length range.

swift
1import UIKit
2
3func setCursorOffset(_ offset: Int, in textField: UITextField) {
4    let clampedOffset = max(0, min(offset, textField.text?.count ?? 0))
5
6    guard let position = textField.position(
7        from: textField.beginningOfDocument,
8        offset: clampedOffset
9    ) else {
10        return
11    }
12
13    textField.selectedTextRange = textField.textRange(from: position, to: position)
14}

Clamping matters because offsets beyond the current text length simply fail to produce a valid position.

Doing the Same for UITextView

UITextView uses the same cursor model, so the code is almost identical.

swift
1import UIKit
2
3func cursorOffset(in textView: UITextView) -> Int? {
4    guard let selectedRange = textView.selectedTextRange else {
5        return nil
6    }
7
8    return textView.offset(
9        from: textView.beginningOfDocument,
10        to: selectedRange.start
11    )
12}
13
14func setCursorOffset(_ offset: Int, in textView: UITextView) {
15    let clampedOffset = max(0, min(offset, textView.text.count))
16
17    guard let position = textView.position(
18        from: textView.beginningOfDocument,
19        offset: clampedOffset
20    ) else {
21        return
22    }
23
24    textView.selectedTextRange = textView.textRange(from: position, to: position)
25}

Because both controls share the same UITextInput concepts, the main difference is usually how your screen uses them, not how cursor movement works.

Preserving Cursor Position During Formatting

One common use case is live formatting, such as inserting spaces into a phone number or currency field. Without manual cursor handling, the caret often jumps to the end after you assign new text.

swift
1import UIKit
2
3final class PhoneFormatter {
4    static func format(_ raw: String) -> String {
5        let digits = raw.filter(\.isNumber)
6        var result = ""
7
8        for (index, digit) in digits.enumerated() {
9            if index == 3 || index == 6 {
10                result.append("-")
11            }
12            result.append(digit)
13        }
14
15        return result
16    }
17}
18
19func reformat(textField: UITextField) {
20    let originalOffset = cursorOffset(in: textField) ?? 0
21    let originalText = textField.text ?? ""
22
23    textField.text = PhoneFormatter.format(originalText)
24    setCursorOffset(originalOffset, in: textField)
25}

In real masked-input code, you usually adjust the offset to account for inserted or removed formatting characters.

Working with Selections, Not Just a Caret

Sometimes you need to preserve a selection range rather than a single insertion point. The same idea applies: convert offsets into positions and create a range between them.

swift
1import UIKit
2
3func setSelection(start: Int, end: Int, in textView: UITextView) {
4    let safeStart = max(0, min(start, textView.text.count))
5    let safeEnd = max(safeStart, min(end, textView.text.count))
6
7    guard
8        let startPosition = textView.position(from: textView.beginningOfDocument, offset: safeStart),
9        let endPosition = textView.position(from: textView.beginningOfDocument, offset: safeEnd),
10        let range = textView.textRange(from: startPosition, to: endPosition)
11    else {
12        return
13    }
14
15    textView.selectedTextRange = range
16}

That is useful for editor-like features, mentions, or guided text replacement.

Common Pitfalls

The most common mistake is treating cursor position as raw string indexing and ignoring UIKit's document position APIs. Another is setting text and expecting the old cursor position to survive automatically. Developers also forget to clamp offsets, which makes position creation fail silently. In formatted inputs, restoring the old offset without accounting for inserted separators can still produce a cursor jump, so offset adjustment logic needs to match the formatter.

Summary

  • 'UITextField and UITextView expose cursor control through selectedTextRange and document positions.'
  • Use offset(from:to:) to read the current cursor position.
  • Use position(from:offset:) plus textRange(from:to:) to set the cursor or selection.
  • Clamp offsets to valid text length before restoring positions.
  • Preserve and adjust cursor state explicitly when modifying text programmatically.

Course illustration
Course illustration

All Rights Reserved.