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?