19

I am using a SwiftUI TextField with a Binding String to change the user's input into a phone format. Upon typing, the formatting is happening, but the cursor isn't moved to the end of the textfield, it remains on the position it was when it was entered. For example, if I enter 1, the value of the texfield (after formatting) will be (1, but the cursor stays after the first character, instead of at the end of the line.

Is there a way to move the textfield's cursor to the end of the line?

Here is the sample code:

import SwiftUI
import AnyFormatKit

struct ContentView: View {
    @State var phoneNumber = ""
    let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")

    var body: some View {

    let phoneNumberProxy = Binding<String>(
        get: {
            return (self.phoneFormatter.format(self.phoneNumber) ?? "")
        },
        set: {
            self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
        }
    )

        return TextField("Phone Number", text: phoneNumberProxy)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
njdeveloper
  • 1,465
  • 3
  • 13
  • 16

3 Answers3

12

You might have to use UITextField instead of TextField. UITextField allows setting custom cursor position. To position the cursor at the end of the text you can use textField.endOfDocument to set UITextField.selectedTextRange when the text content is updated.

@objc func textFieldDidChange(_ textField: UITextField) {        
    let newPosition = textField.endOfDocument
    textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}

The following SwiftUI code snippet shows a sample implementation.

import SwiftUI
import UIKit
//import AnyFormatKit

struct ContentView: View {
    @State var phoneNumber = ""

    let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")

    var body: some View {

    let phoneNumberProxy = Binding<String>(
        get: {
            return (self.phoneFormatter.format(self.phoneNumber) ?? "")
        },
        set: {
            self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
        }
    )

        return TextFieldContainer("Phone Number", text: phoneNumberProxy)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

/************************************************/

struct TextFieldContainer: UIViewRepresentable {
    private var placeholder : String
    private var text : Binding<String>

    init(_ placeholder:String, text:Binding<String>) {
        self.placeholder = placeholder
        self.text = text
    }

    func makeCoordinator() -> TextFieldContainer.Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: UIViewRepresentableContext<TextFieldContainer>) -> UITextField {

        let innertTextField = UITextField(frame: .zero)
        innertTextField.placeholder = placeholder
        innertTextField.text = text.wrappedValue
        innertTextField.delegate = context.coordinator

        context.coordinator.setup(innertTextField)

        return innertTextField
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
        uiView.text = self.text.wrappedValue
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: TextFieldContainer

        init(_ textFieldContainer: TextFieldContainer) {
            self.parent = textFieldContainer
        }

        func setup(_ textField:UITextField) {
            textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
        }

        @objc func textFieldDidChange(_ textField: UITextField) {
            self.parent.text.wrappedValue = textField.text ?? ""

            let newPosition = textField.endOfDocument
            textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
        }
    }
}
ddelver
  • 456
  • 3
  • 9
  • Does this solution approach extrapolate to TextEditor as well? Curious if its worth a try. – Scott Apr 16 '21 at 05:26
0

Unfortunately I can't comment on ddelver's excellent answer, but I just wanted to add that for me, this did not work when I changed the bound string.

My use case is that I had a custom text field component used to edit the selected item from a list, so as you change selected item, the bound string changes. This meant that TextFieldContainer's init method was being called whenever the binding changed, but parent inside the Coordinator still referred to the initial parent.

I'm new to Swift so there may be a better fix for this, but I fixed it by adding a method to the Coordinator:

func updateParent(_ parent : TextFieldContainer) {
    self.parent = parent
}

and then calling this from func updateUIView like:

func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
    uiView.text = self.text.wrappedValue
    context.coordinator.updateParent(self)
}
0

You can do something like this:

final class ContentViewModel: ObservableObject {
    private let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
    private var realPhoneNumber = ""
    @Published var formattedPhoneNumber = "" {
        didSet {
            let formattedText = phoneFormatter.format(formattedPhoneNumber) ?? ""
            
            // Need this check to avoid infinite loop
            if formattedPhoneNumber != formattedText {
                let realText = phoneFormatter.unformat(formattedPhoneNumber) ?? ""
                formattedPhoneNumber = formattedText
                realPhoneNumber = realText
            }
        }
    }
}

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()

    var body: some View {
        return TextField("Phone Number", text: $viewModel.formattedPhoneNumber)
    }
}

The idea here is that when you manually set (assign) the text binding, the cursor of the textField moves to the end of the text.

narek.sv
  • 845
  • 5
  • 22