How to correctly move a TextField selection cursor when inserting UTF16 text

I'm trying to implement a feature whereby a user can tap a button to insert a character at the cursor in a TextField - the cursor then needs to be moved forward to be in front of the insert character.

I'm having trouble with characters such as π which are UTF16 encoded.

In the following sample app, enter the following sequence:

  • Enter 9 by keyboard
  • tap +
  • Enter 9 by keyboard
  • tap π
  • Enter 9 via keyboard
  • tap +

he TextField will show '9+9π+9' (i.e. the final + is inserted before 9 rather than after it. Any insight into what I am doing wrong?

import SwiftUI

@main
struct TextInsertApp: App {
	var body: some Scene {
		WindowGroup {
			ContentView()
		}
	}
}

struct ContentView: View {
	
	@State private var text: String = ""
	@State private var selection: TextSelection? = nil
	
    var body: some View {
		TextField("", text: $text, selection: $selection)
			.background(.gray.opacity(0.4))
		Button("+") { insert("+") }
		Button("π") { insert("π") }
    }
	
	func insert(_ insertString: String) {
		if let selection {
			if case let .selection(range) = selection.indices {
				if selection.isInsertion {
					text.insert(contentsOf: insertString, at: range.lowerBound)
				} else {
					text.replaceSubrange(range, with: insertString)
				}
				let cursor = text.utf16.index(range.upperBound, offsetBy: insertString.count)
				self.selection = .init(insertionPoint: cursor)
			}
		} else {
			text += insertString
			selection = .init(range: text.utf16.endIndex..<text.utf16.endIndex)
		}
	}
}

It seems that TextField doesn't correctly update the text selection after the "π" and the following "9" are inputted, which seems to be a bug. Do you have a feedback report yet? If not, would you mind to file one and share your report ID here?

This doesn't change anything, but is probably worth mentioning: when calculating the new cursor position, using .utf16 doesn't make a lot of sense. Rather, I'd use the distance of the selection + text.startIndex, as shown below:

if case let .selection(range) = selection.indices {
    let offset = text.distance(from: text.startIndex, to: range.lowerBound)
    ...
    let cursor = text.index(text.startIndex, offsetBy: offset + insertString.count)    
    self.selection = .init(insertionPoint: cursor)
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

How to correctly move a TextField selection cursor when inserting UTF16 text
 
 
Q