iOS Password AutoFill适配经验

Wan Xiao
15 min readJan 30, 2024

--

最近我的工作从 Android 开发转为 iOS 开发。第一个需求就是为我们的 app 适配 Password AutoFill,让用户可以将账号密码记在 iOS 里,避免总是忘记密码。

出乎我意料的是,Password AutoFill 的使用有非常多的限制和 bug,且需要处理一些意想不到的情况。如果没有正确适配,极有可能导致用户的账号密码不被保存或者保存一个错误的账号密码。

注意本文中的“用户名”,指的是用户登录用的用户名,而非用户昵称或真实名字,下文不再说明。

在 iOS 中启用 Password AutoFill

以 iOS 17 为例,Password AutoFill 需要在 Settings — Passwords — Password Options 中,打开 AutoFill Passwords and Passkeys。

与此同时,iOS 中还有一个自动生成并填充强密码的功能,需要在 Settings — 顶部 iCloud Account — iCloud中,确保 Passwords and Keychain 处于打开的状态。

这样 iOS 就有了自动生成强密码和自动填充账户密码的能力。

App 中适配 Password AutoFill

App 必须有关联域名,可以参考 Apple 的文档 Supporting associated domains

按照 Apple 的文档 Enabling Password AutoFill on a text input view 中所描述的,只需要给对应的 UITextField 设置好正确的 UITextContentType 即可让 Password AutoFill 正确工作。可能的 UITextContentType 如下:

UITextContentType value

当 UITextContentType == UITextContentTypeNewPassword 时,iOS 会自动生成建议的强密码:

Strong password

如果你希望 iOS 可以生成满足你的 App 要求的密码,可以使用 passwordRules 来告知 iOS 生成密码的规则。例如下面的规则,是要求密码最小10个字符,最大14个字符,要求必须有小写、大写及数字:

minlength: 10; maxlength: 14; required: lower; required: upper; required: digit;

规则可以通过 Apple 的网站生成或验证:https://developer.apple.com/password-rules/

注意 iOS 只有在文本框的 isSecureTextEntry 属性为 YES 时,才会建议强密码。

上面介绍的都是 iOS 官方或者目前网络上可以查到的信息,但实际上是配过程中,结合业务代码,还有各种问题需要处理。

Password AutoFill 大概的工作方式

根据 Apple 的文档,以及我实际测试的结果,iOS Password AutoFill 采用启发式的方法判断什么时候用户是在登录,什么时候用户是在创建密码,哪个文本框是用户名,哪个文本框是密码,哪个文本框是新密码。Password AutoFill 将会在具有相关文本框的 UIViewController 关闭时,执行相应的操作,包括保存/更新密码,某些情况下还会弹出 UIActionSheet 询问用户是否需要保存/更新密码。

Password AutoFill 在 iOS 原生 App 运行的时候保存的密码,一般具有三个字段:用户名(Username),密码(Password),关联域名(Associated domains)。可以在 Settings — Passwords 中查看。

关联域名(Associated domains)可以参考 Apple 的文档 Supporting associated domains。这一项是系统自动根据 App 的关联域名确定的,且关联域名必须能通过校验,否则 Password AutoFill 不会工作。开发者无法指定,更无法冒用其他 App 的关联域名。当用户在 Safari 中浏览这个域名或子域名的页面,需要输入密码时,Safari 会自动提示填充从 App 中保存的密码。

由于是启发式的工作方式,难免有判断错误的时候,因此 iOS 还允许开发者设置 UITextField 的 UITextContentType 属性,显式指定文本框的具体作用。

但遗憾的是,根据我的测试,即便是到了 iOS 17,Password AutoFill 也仅仅是参考开发者设置的 UITextContentType,大部分情况下还是优先使用启发式的方法工作,这就导致某些情况下,即便你设置了 UITextContentType 也没用,甚至有时还需要设置错误的 UITextContentType 来让 Password AutoFill 正常工作。

用户需要保存/更新密码的场景

用户一般在如下场景需要保存/更新密码。

  • 注册账号后首次设置密码
  • 在新设备上首次登录
  • 更改密码
  • 更改登录账号

由于 Password AutoFill 仅从一个 App 中保存用户名和密码,并没有 userId 这样的概念,所以当用户更改登录账号后重新登录时,Password AutoFill 会将其视为另一个账号密码记住。

Password AutoFill 不知道用户登录/注册成功了

与 Google 的 One Tap for Android 不同,App 并不需要将用户名和密码直接告知 iOS。Password AutoFill 只是在以启发式的方式工作,它并不知道用户登录/注册是否成功了,这个信息只有 App 才知道,而且 Apple 也并没有提供任何接口给 App 来告知系统。

Password AutoFill 仅在显示有用户名密码的输入框的 UIViewController 关闭时,提示用户保存/更新密码。假如用户在登录页面输入了用户名和密码,但又没有登录,直接退出登录页面,此时系统也会提醒保存密码,如果用户输入了错误的密码,就可能会被保存下来。

因此在适配 Password AutoFill 时,很重要的一点就是当用户没有真正的执行密码验证流程,直接退出用户名及密码界面时,需要清空 UITextField 里的内容,避免 Password AutoFill 提示用户保存。

隐形的用户名 UITextField

很多现代 App 的登录流程,往往都是先输入用户名,后台判断为已存在用户后再打开一个仅有密码输入框的页面。

刚才提到 Password AutoFill 是以启发式的方式工作,但在 iOS 17 上,它仍旧只有在 UIViewController 中同时存在用户名输入框和密码输入框时,才能正常工作,如果你的页面上仅有一个密码输入框,Password AutoFill 并不能正常工作。

解决方案是在密码输入框的正上方,添加一个用户不可交互(userInteractionEnabled = NO)的 UITextField,并在其中填充用户的用户名,比如前一个页面用户输入的用户名。在一个原本没有用户名输入框的页面,添加一个输入框展示用户名,会改变页面的设计,为了避免改变页面的设计,我们可以将这个不可交互的 UITextField 隐藏起来,注意这里不能用 hidden 属性来隐藏 UITextField,Password AutoFill 并不识别被隐藏的 UITextField。

UITextField *tf = UITextField.new;
tf.text = {username for login};
tf.textContentType = UITextContentTypeUsername;
tf.textColor = {same color as your background};
tf.userInteractionEnabled = NO;

但我们可以让 UITextField 对用户是隐藏的,但对系统是可见的,实现也很简单,那就是将 UITextField 的文本颜色设置为和背景一致。

注意 UI 布局上,将存放用户名的 UITextField 放在密码 UITextField 的正上方,如果是更改密码页面,则应该是新密码的正上方。

避免用户昵称被保存为用户名

某些 App 的注册流程,可能是先 OTP 验证手机号,再让用户设置昵称和密码,UI 界面可能是这样排版的:

用户昵称输入框
用户密码输入框

此时 Password AutoFill 会将密码框上方的昵称文本框视为用户名保存,这会导致错误。

因此你需要在密码框上方放一个隐形的用户名 UITextField,并填充用户的用户名,这样 Password AutoFill 才能保存正确的用户名。

也就是说,最终你的 UI 界面应该是这样排版的:

用户昵称输入框
用户名输入框(隐形且不可编辑)
用户密码输入框

注意密码输入框上方的第一个 UITextField 必须是你的用户名输入框,他们中间不能有任何其他 UITextField,因为 Password AutoFill 总是将密码框上方出现的第一个输入框作为用户名输入框,就算你没有指定 UITextContentType 也一样。

避免用户名 UITextField 被修改

隐形的用户名 UITextField 中需要填充正确的用户名,才能正常工作。你还需要避免你存放用户名的 UITextField 被修改。

虽然用户无法修改,但系统输入法却有能力修改,iOS 的输入法有时会提示用户自动填充用户名,如果用户点击了系统提供的选项,则系统会直接修改用户名 UITextField,导致我们保存一个错误的用户名,我们需要将它改回来,最简单的就是监听文本改变的事件,一旦发现和我们想要的不符,就改回来:

// UITextField *tf = UITextField.new;
// self.usernameForAutoFill = {username for login}
[tf addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];

- (void)textFieldDidChange:(UITextField *)textField {
if (textField == self.usernameTFForAutoFill) {
if (![textField.text isEqualToString:self.usernameForAutoFill]) {
textField.text = self.usernameForAutoFill;
}
}
}

isSecureTextEntry 为 NO 时可能导致的问题

当今许多 App 的密码输入框都有明文展示密码的按钮,在点击之后密码会以明文展示:

带有展示密码按钮的密码输入框

这往往是通过改变 UITextField 的 isSecureTextEntry 属性实现的:

- (void)onTap:(UIButton *)sender {
self.passwordTextField.secureTextEntry = !self.passwordTextField.isSecureTextEntry;
}

但这个功能可能会导致 iOS Password AutoFill 不按你预期的方式工作。如果你仔细观察的话,你会发现 iOS 系统内很多密码输入框,是不支持明文展示密码的。iOS Password AutoFill 会通过 secureTextEntry 来判断一个 UITextField 是否是密码框,而不仅仅是 UITextContentType。只有当 Password AutoFill 认为你的密码文本框是它所认可的密码框时,才能正常工作。

由于我只在 iOS 16 和 iOS 17 上测试过 Password AutoFill,我只知道 iOS 16 和 iOS 17 上的表现,具体如下:

iOS 16
如果 UITextField 在 UIViewController 关闭时,secureEntryText为 NO,则 Password AutoFill 不认为它是密码框。

iOS 17
如果 UITextField 在用户点击它时 secureEntryText 为 YES,Password AutoFill 就会认为它是密码框,就算后续它的 secureEntryText 不为 YES,Password AutoFill 也会记得它。

但如果是 UITextContentTypeNewPassword 创建新密码的情况,则至少要有一个新密码文本框在 UIViewController 关闭时 secureEntryText 为 YES,Password AutoFill 才会提示保存密码。

所以我建议最好将展示密码按钮改为按住时展示明文,松手时恢复展示密文的形式,这样不太容易出问题。

最好在向服务器发起请求前,将密码框的 secureTextEntry 全部主动改为 YES。

iOS 16 重复建议强密码问题

UITextContentTypeNewPassword 用于新密码的 UITextField,此时 iOS 在满足条件时会自动生成建议的强密码,但用户也可以通过点击“Choose My Own Password”来自己设置密码。

一般设置密码的页面,都会要求用户重复输入一次新密码。在 iOS 16 上,有奇怪的 bug,当用户点击第一个新密码输入框,选择“Choose My Own Password”,输入了密码之后,点击第二个新密码输入框时,iOS 16 会再次建议强密码,用户体验非常差。

有一种办法可以绕过这个 bug,那就是将第二个新密码输入框的 UITextContentType 设置为 UITextContentTypePassword。

也就是说,在 iOS 17 上,你的文本框是这样的:

UITextField (UITextContentTypeUsername)
UITextField (UITextContentTypeNewPassword)
UITextField (UITextContentTypeNewPassword)

而在 iOS 16 上,需要改成:

UITextField (UITextContentTypeUsername)
UITextField (UITextContentTypeNewPassword)
UITextField (UITextContentTypePassword)

Password AutoFill 的启发式工作方式,会正确的将第二个 UITextField 也识别为新密码输入框。

Password AutoFill 不保存新密码问题

iOS Password AutoFill 还有一个非常恶心的问题,如果用户最后一个编辑的不是 UITextContentTypeNewPassword 对应的 UITextField,则 Password AutoFill 不会保存新密码。

因此你需要在注册、修改密码、重置密码这些页面上,在即将向服务器发起请求时,检查用户最后一次编辑的是不是 UITextContentTyp 为 eUITextContentTypeNewPassword 的 UITextField,如果不是,则需要主动将其 becomeFirstResponder,让用户再点击一次按钮触发请求。

可以通过 UITextFieldDelegate 中的 textFieldDidBeginEditing 来记录最后一次被编辑的 UITextField,如果不是 UITextContentTypeNewPassword 对应的 UITextField,则让 becomeFirstResponder。

[newPasswordTextField becomeFirstResponder];

注意此时千万不要立即调用 resignFirstResponder,来尝试将弹起的软键盘收回,否则 iOS 会生成一个新的强密码。

我对于 iOS Password AutoFill 的看法

原本我希望用户可以通过 Password AutoFill 方便的记住密码,但实际开发过程中遇到的一系列问题,让我觉得 Password AutoFill 非常不可靠,而且严重时还会导致问题。因此在完成对 Password AutoFill 的支持后,我将目标改为在 App 内记住用户的密码,将密码保存到用户的 Keychain 中,以带来更好的用户体验,同时在密码存储安全性上也能得到 iOS 的保护。

--

--