uitextfield键盘
by Roland Leth
罗兰·莱斯(Roland Leth)
如何在UITextField焦点上管理键盘以获得更好的用户体验 (How to manage the keyboard on UITextField focus for a better user experience)
A couple of posts ago I was writing about handling the Next button automatically. In this post I’d like to write about avoiding the keyboard automatically, in a manner that provides both a good user experience and a good developer experience.
几篇文章之前,我在写有关自动处理“下一步”按钮的文章。 在这篇文章中,我想写一篇关于自动避开键盘的方法,这种方式既要提供良好的用户体验,又要提供良好的开发人员体验。
Most apps have some sort of form that needs to be filled, even if just a login/register, if not several forms. As a user, having the keyboard cover the text field I’m about to fill makes me sad — it’s a poor user experience. As developers, we’d like to solve this as easily as possible and have the solution be as reusable as possible.
大多数应用程序都有某种形式的表单,即使不是一个登录/注册,也需要填写。 作为用户,让键盘覆盖我要填写的文本字段会让我感到难过-这是糟糕的用户体验。 作为开发人员,我们希望尽可能轻松地解决此问题,并使解决方案尽可能重用。
What does a good user experience mean?
良好的用户体验是什么意思?
The focused
UITextField
is brought above the keyboard on focus.重点突出的
UITextField
置于焦点上方的键盘上方。The focused
UITextField
is “sent back” on dismiss.聚焦后的
UITextField
在关闭时会“发送回”。
What does a good developer experience mean? Everything should happen as automatically as possible, so we’ll go with a protocol once again. What does this protocol need to encapsulate?
良好的开发人员经验意味着什么? 一切都应尽可能自动发生,因此我们将再次使用协议。 该协议需要封装什么?
- Observing the keyboard will show/hide notifications. 观察键盘将显示/隐藏通知。
On keyboard appearance, it needs to modify the
scrollView.contentInset
andscrollView.contentOffset
in a way that brings theUITextField
right above the keyboard.在键盘外观上,它需要以将
UITextField
置于键盘上方的方式来修改scrollView.contentInset
和scrollView.contentOffset
。- On keyboard disappearance, it needs to reset the inset and offset to previous values. 键盘消失时,需要将插图和偏移量重置为以前的值。
With this in mind, let’s build our protocol:
考虑到这一点,让我们构建协议:
protocol KeyboardListener: AnyObject { // 1
var scrollView: UIScrollView { get } // 2 var contentOffsetPreKeyboardDisplay: CGPoint? { get set } // 3 var contentInsetPreKeyboardDisplay: UIEdgeInsets? { get set } // 4
func keyboardChanged(with notification: Notification) // 5
}
We need to constrain this protocol to be conformed to only by classes (1) because we’ll need to modify the two preKeyboard
properties (3, 4). We’ll use them to know how to revert the scrollView
’s inset and offset on keyboard dismissal. We’ll most likely implement this in a UIViewController
anyway.
我们需要限制此协议仅符合类(1),因为我们需要修改两个preKeyboard
属性(3、4)。 我们将使用它们来了解如何在scrollView
键盘时还原scrollView
的插入和偏移。 无论如何,我们很可能会在UIViewController
实现它。
The protocol also needs to have a scrollView
(2), otherwise this isn’t really … feasible (I guess it could be doable). Lastly, we need the method that will handle everything (5), but it just acts as a proxy for two helpers that we’ll implement in just a bit:
该协议还需要有一个scrollView
(2),否则这实际上是不可行的(我想这是可行的 )。 最后,我们需要可以处理所有内容的方法(5),但它只是作为我们将要实现的两个助手的代理:
extension KeyboardListener {
func keyboardChanged(with notification: Notification) { guard notification.name == UIResponder.keyboardWillShowNotification, let rawFrameEnd = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey], let frameEnd = rawFrameEnd as? CGRect, let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval else { resetScrollView() // 1
return }
if let currentTextField = UIResponder.current as? UITextField { updateContentOffsetOnTextFieldFocus(currentTextField, bottomCoveredArea: frame.height) // 2 }
scrollView.contentInset.bottom += frameEnd.height // 3 }
}
If the notification is not for willShow
, or we can not parse the notification’s userInfo
, bail out and reset the scrollView
. If it is, increase the bottom inset by the keyboard’s height (3). As for (2), we find the current first responder with a little trick to call updateContentOffsetOnTextFieldFocus(_:bottomCoveredArea:)
with, but we could also call it from our delegate’s textFieldShouldBeginEditing(_:)
.
如果该通知不是针对willShow
,或者我们无法解析该通知的userInfo
,请退出并重置scrollView
。 如果是这样,请通过键盘高度(3)增加底部插图。 至于(2),我们找到了当前的第一个响应者,并带有一些技巧来调用updateContentOffsetOnTextFieldFocus(_:bottomCoveredArea:)
,但是我们也可以从委托人的textFieldShouldBeginEditing(_:)
调用它。
The first helper will update our two preKeyboard
properties:
第一助手将更新我们的两个preKeyboard
属性:
extension KeyboardListener where Self: UIViewController { // 1
func keyboardChanged(with notification: Notification) { // [...] }
func updateContentOffsetOnTextFieldFocus(_ textField: UITextField, bottomCoveredArea: CGFloat) { let projectedKeyboardY = view.window!.frame.minY - bottomCoveredArea // 2
if contentInsetPreKeyboardDisplay == nil { // 3 contentInsetPreKeyboardDisplay = scrollView.contentInset } if contentOffsetPreKeyboardDisplay == nil { // 4 contentOffsetPreKeyboardDisplay = scrollView.contentOffset }
let textFieldFrameInWindow = view.window!.convert(textField.frame, from: textField.superview) // 5 let bottomLimit = textFieldFrameInWindow.maxY + 10 // 6
guard bottomLimit > projectedKeyboardY else { return } // 7
let delta = projectedKeyboardY - bottomLimit // 8 let newOffset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y - delta) // 9
scrollView.setContentOffset(newOffset, animated: true) // 10 }
}
We will now update the protocol extension with a Self: UIViewController
constraint (1), because we’ll need access to the window. This shouldn’t be an inconvenience, because this protocol will be most likely used by UIViewController
s. However, another approach would be to replace all the view.window
occurrences with UIApplication.shared.keyWindow
or a variation of UIApplication.shared.windows[yourIndex]
, in case you have a complex hierarchy.
现在,我们将使用Self: UIViewController
约束(1)更新协议扩展,因为我们需要访问该窗口。 这不应该带来不便,因为UIViewController
很有可能会使用此协议。 但是,另一种方法是在层次结构复杂的情况下,将所有view.window
出现的内容替换为UIApplication.shared.keyWindow
或UIApplication.shared.windows[yourIndex]
的变体。
We then calculate the minY
for the keyboard (2) — we use a parameter for those cases where we have a custom inputView
and we’ll call this from textFieldShouldBeginEditing(_:)
, for example. We then check if our preKeyboard
properties are nil
. If they are, we assign the current values from the scrollView
(3, 4). They might not be nil
if we changed them prior to calling this method.
然后,我们计算出minY
的键盘(2) -我们使用那些我们有一个自定义的情况下的参数inputView
我们将从调用这个textFieldShouldBeginEditing(_:)
,例如。 然后,我们检查preKeyboard
属性是否为nil
。 如果是的话,我们从scrollView
(3,4)分配当前值。 如果在调用此方法之前更改它们,它们可能不会nil
。
We then convert the textField
’s maxY
in the window’s coordinates (5) and add 10
to it (6), so we have a small padding between the field and the keyboard. If the bottomLimit
is above the keyboard’s minY
, do nothing, because the textField
is already fully visible (7). If the bottomLimit
is below the keyboard’s minY
, calculate the difference between them (8) so we know how much to scroll the scrollView
(9, 10) so that the textField
will be visible.
然后,我们在窗口的坐标(5)中转换textField
的maxY
并为其添加10
(6),因此在字段和键盘之间会有一个小的填充。 如果bottomLimit
在键盘的minY
,则不执行任何操作,因为textField
已经完全可见(7)。 如果bottomLimit
在键盘的minY
,请计算它们之间的差(8),以便我们知道滚动scrollView
(9,10)多少,以便textField
可见。
The second helper resets our scrollView
back to the initial values:
第二个助手将我们的scrollView
重置回初始值:
extension KeyboardListener where Self: UIViewController {
func keyboardChanged(with notification: Notification) { // [...] }
func updateContentOffsetOnTextFieldFocus(_ textField: UITextField, bottomCoveredArea: CGFloat) { // [...] }
func resetScrollView() { guard // 1 let originalInsets = contentInsetPreKeyboardDisplay, let originalOffset = contentOffsetPreKeyboardDisplay else { return }
scrollView.contentInset = originalInsets // 2 scrollView.setContentOffset(originalOffset, animated: true) // 3
contentInsetPreKeyboardDisplay = nil // 4 contentOffsetPreKeyboardDisplay = nil // 5 }
}
If we have no original insets/offset, do nothing; for example, a hardware keyboard is used (1). If we do, we reset the scrollView
to its original, pre-keyboard values (2, 3) and nil
-out the preKeyboard
properties (4, 5).
如果我们没有原始的插图/偏移,则不执行任何操作。 例如,使用硬件键盘(1)。 如果这样做,则将scrollView
重置为其原始的键盘前值(2、3),然后将preKeyboard
属性设置为nil
preKeyboard
(5)。
Using this may vary depending on your needs, but the usual scenario would go like this:
根据您的需要,使用此方法可能会有所不同,但是通常情况如下:
final class FormViewController: UIViewController, KeyboardListener {
let scrollView = UIScrollView() /* Or if you have a tableView: private let tableView = UITableView() var scrollView: UIScrollView { return tableView } */
// [...]
override func viewDidLoad() { super.videDidLoad()
let center = NotificationCenter.default
center.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] notification in self?.keyboardChanged(with: notification) }
center.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] notification in self?.keyboardChanged(with: notification) }
// And that's it! }
// [...]
}
This was a lot of info, but we now have a nice ”keep the text field above the keyboard” logic. If we implement all of this alongside the automatic Next button handling, it will be like magic for our users.
这是很多信息,但是我们现在有了一个很好的“将文本字段保留在键盘上方”的逻辑。 如果我们在自动执行“下一步”按钮处理的同时实现所有这些功能 ,那么对于我们的用户来说,这就像是魔术。
Check out this post about slightly automating this even further, by implementing the Broadcaster/Listener system and moving the observers in the Broadcaster
itself. We wouldn’t need to add observers in our view controllers anymore, we’d just have to call Broadcaster.shared.addListener(self)
.
通过实现Broadcaster / Listener系统并在Broadcaster
本身中移动观察者,来查看有关稍微进一步自动化的文章 。 我们不再需要在视图控制器中添加观察者,只需要调用Broadcaster.shared.addListener(self)
。
As usual, I’d love to hear your thoughts @rolandleth.
和往常一样,我很想听听你的想法。
Originally published at rolandleth.com on October 18, 2018.
最初于2018年10月18日在rolandleth.com上发布。
uitextfield键盘