I have a custom property wrapper I am binding to a UIViewRepresentable of a textField. When the user types in the textfield the UIViewRepresentable's updateUIView is called at least two times and textfieldbeginediting within my coordinator is also called multiple times. This was not occurring in ios 14 and can produce undesired results. Especially with my text limit logic, textfieldBeginEditing happens every time the user types which causes textfield to remove text. But once i remove my propertyWrapper i get desired effect and textFieldShouldBeginEditing is not called multiple times
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
print("textfiled begin editign")
if textLimit != nil{
textField.text = ""
}
return true
}
Here's a similar question https://developer.apple.com/forums/thread/681947
TextField
struct TextField: UIViewRepresentable {
@Binding var text: String
//@Binding var textStyle: UIFont.TextStyle
@Binding var isSecureEntry: Bool
@Binding var isEditing: Bool
var placeHolder: String = ""
var keyboardType: UIKeyboardType = .default
var tag: Int = 0
var keyboardReturnType: UIReturnKeyType = .next
var textfieldContentType: UITextContentType? = nil
var textAlignment: NSTextAlignment = .left
var textLimit: Int? = nil
var moveToNextTextFieldOnTextLimit = false
func makeUIView(context: Context) -> UITextField {
print("make ui view")
let textField = UITextField(frame: .zero)
//must add or textfield will grow beyond bounds of screen
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.placeholder = placeHolder
textField.tag = tag
textField.delegate = context.coordinator
textField.textAlignment = textAlignment
textField.keyboardType = keyboardType
textField.isSecureTextEntry = isSecureEntry
textField.returnKeyType = keyboardReturnType
textField.textContentType = textfieldContentType
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
print("update view")
uiView.text = text
uiView.isSecureTextEntry = isSecureEntry
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, isEditing: $isEditing, textLimit: textLimit, moveToNextTextFieldOnTextLimit: moveToNextTextFieldOnTextLimit)
}
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
@Binding var isEditing: Bool
var textLimit: Int?
var moveToNextTextFieldOnTextLimit: Bool
// var didBecomeFirstResponder = false
init(text: Binding<String>, isEditing: Binding<Bool>, textLimit: Int?, moveToNextTextFieldOnTextLimit: Bool) {
_text = text
_isEditing = isEditing
self.textLimit = textLimit
self.moveToNextTextFieldOnTextLimit = moveToNextTextFieldOnTextLimit
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.text = textField.text ?? ""
}
if let textLimit = textLimit, moveToNextTextFieldOnTextLimit{
if textField.text?.count == textLimit{
firstResponderToNextTextField(textField: textField)
}
}
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
print("textfiled begin editign")
if textLimit != nil{
textField.text = ""
}
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async { [weak self] in
self?.isEditing = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async { [weak self] in
self?.isEditing = false
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
firstResponderToNextTextField(textField: textField)
return false
}
func firstResponderToNextTextField(textField: UITextField){
if let nextField = textField.superview?.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
//TODO can implement action here so when user hits finaly text field we could have them sign in or action similar.
textField.resignFirstResponder()
}
}
}
}
Property Wrapper
@propertyWrapper
struct PhoneNumberFormater{
private(set) var defaultValue: String = ""
var error: EvaluatePhoneNumberError? = .isEmpty
//@StateObject private var updater = Updater()
init(wrappedValue initialValue: String) {
self.wrappedValue = initialValue
}
var wrappedValue: String {
get{defaultValue}
set{
if newValue.count == 0 {
error = .isEmpty
}else if !isPasswordLongEnough(password: newValue){
error = .isNotValidLength
}else{
error = nil
}
defaultValue = format(with: "(***) ***-XXXX", phone: newValue)
// updater.notifiyUpdate()
}
}
var projectedValue: PhoneNumberFormater {return self}
private func isPasswordLongEnough(password: String) -> Bool{
password.count >= 14 ? true : false
}
/// mask example: `+X (***) ***-XXXX`
func format(with mask: String, phone: String) -> String {
let numbers = phone.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
var result = ""
var index = numbers.startIndex // numbers iterator
// iterate over the mask characters until the iterator of numbers ends
for ch in mask where index < numbers.endIndex {
if ch == "X" {
// mask requires a number in this place, so take the next one
result.append(numbers[index])
// move numbers iterator to the next index
index = numbers.index(after: index)
} else {
result.append(ch) // just append a mask character
}
}
return result
}
}