1

Given a TextField which should receive a phone, format the number with XX-XX-XXXX pattern, and this specific input field should not allow input over 8 symbols, and only alphanumeric and so on. I'm trying to use @ObservableObject with @Published var, however, when the value is updated with suffix, the text input position does not jump to the end of the input field:

class SignupModel: ObservableObject {
    @Published var phoneNumber: String = "" {
        didSet {
            if (self.phoneNumber.count == 3 || self.phoneNumber.count == 5) {
                if (self.phoneNumber.count == 3) {
                    self.phoneNumber.insert("-", at: String.Index(encodedOffset: 3))
                } else if self.phoneNumber.count == 5 {
                    self.phoneNumber.insert("-", at: String.Index(encodedOffset: 5))
                }
            }
            print(self.phoneNumber)
        }
    }
}

...


@ObservedObject var formModel: SignupModel = SignupModel()

...

TextField("12 3333 123", text: self.$formModel.phoneNumber)

What would be the valid approach to achieve validation and formatting using combine?

Ilja
  • 1,205
  • 1
  • 16
  • 33
  • tried to do that but `newValue` seems to be immutable and modifying the `phoneNumber` value from `willSet` gives segfault – Ilja Jul 21 '20 at 09:07

1 Answers1

2

I ended up implementing didSet along with a custom TextField which places the selector to the end of the input textField:

class SignupModel: ObservableObject {
    @Published var buttonEnabled = false
    @Published var phoneNumber: String = "" {
        didSet {
            if (self.phoneNumber.count == 13) {
                phoneNumber = oldValue
                self.buttonEnabled = true
                return
            }
            buttonEnabled = false
            if ((self.phoneNumber.count == 2 || self.phoneNumber.count == 5 || self.phoneNumber.count == 8)) {
                if (oldValue.last == " ") {
                    phoneNumber.remove(at: oldValue.index(before: phoneNumber.endIndex))
                } else {
                    phoneNumber = phoneNumber + " "
                }
            }
        }
    }
}

Below is a custom TextField container from this post:

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

and I used it just like that:

TextFieldContainer("93 49 99 353", text: self.$formModel.phoneNumber)
Ilja
  • 1,205
  • 1
  • 16
  • 33