Programatically adding to a TextField and moving the TextSelection point in SwiftUI

Hi!

I am trying to create a simple SwiftUI TextField, with an external button to add text to the field at the current insertion point (the cursor in the TextField).

When I add the text, the cursor (I-Beam) remains at the original insertion point, so I want it to move over to the end of what I added. The trouble is, it sometimes moves further forward or to the end (visibly) but works as if it is still at the point I moved it to. This seems to possibly be due to emojis in the TextField (because, I assume, they are composed of more bytes).

Further, sometimes the addition of the text can cause an emoji to appear unexpectedly, I assume because it is combining the bytes in an odd way. So moving the cursor seems to sometimes introduce weird behaviour.

This comes from a much larger project, but I have distilled this down to the smallest example project I could. And I have a video to show how it behaves.

Here's the main part of the code, and I'll attach an Xcode project:

import SwiftUI

struct ContentView: View {
    
    @State private var text: String = "abcdef🧁🧁🧁🧁ghijkl"
    @State private var selectedText: TextSelection?
    
    var body: some View {
        VStack {
            TextField("", text: $text, selection: $selectedText)
                .font(.title)
            Button("Add Z at Insertion Point in TextField") {
                // Get indices of any selection in the text field
                let from: String.Index, to: String.Index
                if let selectedText = selectedText {
                    let indices = selectedText.indices
                    switch indices {
                    case .selection(let range):
                        from = range.lowerBound
                        to = range.upperBound
                    case .multiSelection(let rangeSet):
                        from = rangeSet.ranges.first!.lowerBound
                        to = rangeSet.ranges.first!.upperBound
                    default:
                        from = self.text.endIndex
                        to = self.text.endIndex
                    }
                } else {
                    from = self.text.endIndex
                    to = self.text.endIndex
                }
                
                guard from <= to && from <= self.text.endIndex else { return }
                
                // Insert and update the cursor position
                self.text.replaceSubrange(from..<to, with: "Z")
                
                // Move cursor after the inserted character
                let newIndex = self.text.index(after: from)
                selectedText = TextSelection(insertionPoint: newIndex)
            }
        }
        .padding()
    }
}

STEPS TO REPRODUCE Run the app. Also view the video as it shows the steps.

  1. Put insertion point between c and d.
  2. Press the "Add Z" button. Note that Z is placed between c and d. This is great.
  3. Put insertion point between h and i.
  4. Press the "Add Z" button. Note that Z is placed between h and i. BUT, the insertion point I-beam moves to the end of the string.
  5. Press the "Add Z" button again. Z is added where you would have expected based on where the TextSelection insertion point was put, but the flashing I-Beam is still at the end.
  6. Press the "Add Z" button again. Same issue. insertion point is being shown at end, but to the button it is between Z and i. OF NOTE, if you use the keyboard and press delete, it deletes from end (where the I-beam is).
  7. Now put insertion point between the 4 cupcakes.
  8. Press "Add Z" two times. It behaves correctly.
  9. Press "Add Z" a third time. It adds a fairy emoji.

So, any idea what I am doing wrong? I thought it might be an issue requiring me to update in a background thread, but I tried that, even delaying the update in the thread, but the issue remains.

Thanks in advance.

Here's a video: https://curmi.name/temp/SimpleTextField%20showing%20issues.mp4

And if it helps, here is the Xcode project: https://curmi.name/temp/SimpleTextfield.zip

Programatically adding to a TextField and moving the TextSelection point in SwiftUI
 
 
Q