Swift - Autofill does not populate UITextField

326 Views Asked by At

When a user uses Autofill (not password generation - rather, when they tap a login and use iCloud Keychain to log in - see User taps on Autofill item, if FaceID isn't completed immediately, the UITextField does not populate with the user's username and password. It only fills some of the time.

Since my app is written in SwiftUI, I use a custom TextField.

struct AutoFocusTextField<V: Hashable>: UIViewRepresentable {
    @Binding var text: String
    let placeholder: String
    
    
    var id: V
    @Binding var firstResponder: V?
    
    var onCommit: () -> Void
    var formatText: () -> Void
    
    var inputAccessoryView: UIToolbar? = nil
    
    var secureEntry: Binding<Bool>? = nil
    var returnKeyType: UIReturnKeyType
    var autoCorrection: UITextAutocorrectionType
    var capitalize: UITextAutocapitalizationType
    var keyboardType: UIKeyboardType
    var contentType: UITextContentType?
    var datePicker: UIDatePicker?
        
    init(text: Binding<String>, placeholder: String, id: V, firstResponder: Binding<V?>, onCommit: @escaping (() -> Void) = {}, formatText: @escaping (() -> Void) = {},
         secureEntry: Binding<Bool>? = nil, returnKeyType: UIReturnKeyType = .default, autoCorrection: UITextAutocorrectionType = .no, capitalize: UITextAutocapitalizationType = .none, keyboardType: UIKeyboardType = .default, contentType: UITextContentType? = .none, datePicker: UIDatePicker? = nil) {
        self.id = id
        _text = text
        _firstResponder = firstResponder
        self.placeholder = placeholder
        
        self.onCommit = onCommit
        self.formatText = formatText
        
        
        self.secureEntry = secureEntry
        self.returnKeyType = returnKeyType
        self.autoCorrection = autoCorrection
        self.capitalize = capitalize
        self.keyboardType = keyboardType
        self.contentType = contentType
        self.datePicker = datePicker
        
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text,
                           format: format,
                           onStartEditing: startedEditing,
                           onEndEditing: finishedEditing,
                           onReturnTap: returnTap)
    }
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        
        textField.delegate = context.coordinator
        textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "primary-gray"),
                                                                                               NSAttributedString.Key.font: UIFont(name: "MDPrimer-Regular", size: 16)
                                                                                              ])
        textField.textColor = UIColor(named: "primary-black")
        textField.font = UIFont(name: "MDPrimer-Regular", size: 16)
        textField.tintColor = UIColor(named: "primary-black")
        
        if let datePicker = datePicker {
            textField.inputView = datePicker
            datePicker.datePickerMode = .date
            datePicker.preferredDatePickerStyle = .wheels
            datePicker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged(_:)), for: .valueChanged)
        }
        else {
            textField.keyboardType = keyboardType
        }
        
        textField.tintColor = UIColor(named: "primary-lilac")!
        
        textField.isSecureTextEntry = secureEntry?.wrappedValue ?? false
        textField.autocorrectionType = autoCorrection
        textField.returnKeyType = returnKeyType
        textField.autocapitalizationType = capitalize
        textField.textContentType = contentType ?? .none
        textField.contentVerticalAlignment = .center
        textField.contentHorizontalAlignment = .center
        
        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textField.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
        
        textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
        
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
        if id == firstResponder, uiView.isFirstResponder == false {
            DispatchQueue.main.async {
                uiView.becomeFirstResponder()
            }
        }
    }
    
    func startedEditing() {
        if id != firstResponder {
            firstResponder = id
        }
    }
    
    func finishedEditing() {
        guard id == firstResponder else { return }
        firstResponder = nil
    }
    
    func format() {
        self.formatText()
    }
    
    func returnTap() {
        self.onCommit()
    }
}

protocol TextFieldReturnKeyProtocol {
    func returnTapped()
}


class Coordinator: NSObject, UITextFieldDelegate, TextFieldReturnKeyProtocol {
    @Binding private var text: String
    private let format: (() -> Void)
    private let onStartEditing: (() -> Void)
    private let onEndEditing: (() -> Void)
    private let onReturnTap: (() -> Void)
    var previousText: String?
    var nextText: String?
    
    init(text: Binding<String>, format: @escaping (() -> Void), onStartEditing: @escaping (() -> Void), onEndEditing: @escaping (() -> Void), onReturnTap: @escaping (() -> Void)) {
        _text = text
        self.onStartEditing = onStartEditing
        self.onEndEditing = onEndEditing
        self.onReturnTap = onReturnTap
        self.format = format
        
        super.init()
    }
    
    @objc func textFieldDidChange(_ textField: UITextField) {
        DispatchQueue.main.async { [weak self] in
            self?.text = textField.text ?? ""
            self?.format()
        }
    }
    
    @objc func dateChanged(_ sender: UIDatePicker) {
        let df = DateFormatter()
        df.dateFormat = "MM/dd/yyyy"
        self.text = df.string(from: sender.date)
    }
    
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        onStartEditing()
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        onEndEditing()
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        returnTapped()
        return true
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard var safeText = textField.text else { return true }
        
        DispatchQueue.main.async {
            var str = string
            
            if string.count > 1 && textField.textContentType == .telephoneNumber && textField.keyboardType == .numberPad {
                if string.count > 11 && string.starts(with: "+1") {
                    str.removeFirst(2)
                    textField.text = str
                } else if string.count > 10 && string.starts(with: "1") {
                    str.removeFirst()
                    textField.text = str
                }
            }
        }
        return true
        
        
    }
    
    @objc func returnTapped() {
        onReturnTap()
    }
}

This is wrapped in a larger SwiftUI view.

struct AutoFocusTextFieldWrapper: View {
    @Binding var text: String
    let placeholder: String
    let id: ResponderFields
    @Binding var firstResponder: ResponderFields?
    let onCommit: () -> Void
    
    var hasError: Bool = false
    
    var secureEntry: Binding<Bool>? = nil
    var returnKeyType: UIReturnKeyType = .default
    var autoCorrection: UITextAutocorrectionType = .no
    var capitalize: UITextAutocapitalizationType = .none
    var keyboardType: UIKeyboardType = .default
    var contentType: UITextContentType? = .none
    var datePicker: UIDatePicker? = nil
    
    var formatText: () -> Void
    var enableBorder: Bool = true
    var highlight: Bool = false
    
    var body: some View {
        
        AutoFocusTextField(text: $text, placeholder: placeholder, id: id, firstResponder: $firstResponder, onCommit: onCommit, formatText: formatText, secureEntry: secureEntry, returnKeyType: returnKeyType, autoCorrection: autoCorrection, capitalize: capitalize, keyboardType: keyboardType, contentType: contentType, datePicker: datePicker)
            .padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24))
            .frame(height: 48)
            .frame(maxWidth: .infinity)
    }
}

And then, here is the Login View.

struct LoginView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @EnvironmentObject var loginRegistrationViewModel: LoginRegistrationViewModel
    @EnvironmentObject var viewModel: LoginViewModel

    var dismiss: () -> Void
    
    var body: some View {
        NavigationView {
            VStack(alignment: .center, spacing: 0) {
                NavigationTitle(title: viewModel.navigationTitle, description: viewModel.navigationDescription, backButtonAction: dismiss)
                
                AutoFocusTextFieldWrapper(
                    text: $viewModel.email,
                    placeholder: "Email",
                    id: .loginEmail,
                    firstResponder: $viewModel.firstResponder,
                    onCommit: {
                        viewModel.firstResponder = .loginPassword
                    },
                    hasError: viewModel.hasError,
                    returnKeyType: .next,
                    keyboardType: .emailAddress,
                    contentType: .username,
                    formatText: {},
                    highlight: viewModel.successLoggingIn
                )
                .padding(EdgeInsets(top: 48, leading: 0, bottom: 0, trailing: 0))
                
                AutoFocusTextFieldWrapper(
                    text: $viewModel.password,
                    placeholder: "Password",
                    id: .loginPassword,
                    firstResponder: $viewModel.firstResponder,
                    onCommit: {
                        UIApplication.shared.endEditing()
                    },
                    hasError: viewModel.hasError,
                    secureEntry: .constant(true),
                    returnKeyType: .next,
                    contentType: .password,
                    formatText: {},
                    highlight: viewModel.successLoggingIn
                )
                .padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0))

                
                Spacer()

                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
        
    }

For some reason, the autofill does not always work. I've noticed that if I comment out the code in startedEditing and finishedEditing, it seems to work more consistently. Any idea on why it's not working all the time?

0

There are 0 best solutions below