在连接的控制器中处理键盘事件

最近几个月,我们一直在开发Storgage应用程序(尚未在App Store上),并且我们希望分享开发该应用程序时使用的一种技术。

问题

我们应用程序的功能之一是使用内置的即时消息传递服务,以便用户彼此交谈并查看交易历史记录详细信息。 屏幕的设计如下所示:

如您所见,聊天区位于屏幕底部,当用户点击“键入消息”时,弹出系统键盘将其覆盖。

我们决定,当系统键盘弹出时,应显示一个动画的“键入消息”输入字段,并且应将聊天区域推到屏幕顶部。 退出键盘后(通过点击聊天区域或“发送”按钮),屏幕应返回其原始状态。 这是我们要实现的行为的示例:

问题二

在这一点上,我们面临另一个问题。 消息传递控制器是主屏幕控制器的子级。 家长必须将聊天窗口扩展到全屏状态,我们必须告诉它何时进行。

解决方案二

我们使用委托模式来通知父控制器键盘的出现和消失,以便可以正确地定位内置消息传递控制器。

实作

Storgage项目是用Objective-C编写的,但是为了更好地展示本文中的问题的解决方案,我们将创建一个用Swift编写的示例项目。

首先,让我们创建一个空的“ Single View Application”项目。 将其命名为“ ChildControllerKeyboard”,然后选择Swift作为语言。

然后添加两个类:从UIViewController继承的ContainerViewController和ChildViewController。 为此,请从菜单中选择文件>新建文件…。 然后从弹出窗口中选择“ Cocoa Touch Class”,并使用相应的值填充字段。

对ChildViewController重复上述步骤。 然后可以删除ViewController.swift文件。

现在切换到Main.storyboard并将控制器类从ViewController更改为ContainerViewController。

向主控制器的内容添加视图

将对象库中的视图对象放到控制器上,并设置以下约束,将其附加到屏幕边缘,并将高度设置为等于屏幕的一半:

  • 尾随空格为:Superview = 0
  • 前导空间:Superview = 0
  • 最多空间:Superview = 0
  • 等于以下高度:Superview = 0,Multiplier = 0.5

在“身份检查器”选项卡的“标签”字段中输入值“内容视图”,然后在“属性”检查器选项卡上将“背景”值更改为“浅灰色”。 最后,我们的控制器应如下所示:

添加子控制器

将来自对象库的“容器视图”对象放在控制器上“内容视图”下方,并设置以下约束,并将其附加到屏幕和“内容视图”的底部:

  • 尾随空格为:Superview = 0
  • 前导空间:Superview = 0
  • 底部空间:Superview = 0
  • 最多空间:Content View = 0

将内置控制器的类更改为ChildViewController,然后选择子控制器的“视图”。 在“属性检查器”选项卡中将“背景”值更改为“深灰色”。 然后在对象库中找到“文本字段”对象,并将其放置在子控制器上,通过设置以下约束将其附加到屏幕底部:

  • 尾随空格为:Superview = 0
  • 前导空间:Superview = 0
  • 底部空间:Superview = 0

现在,我们的Main.storyboard应该如下所示:

现在,我们可以切换到ChildViewController文件并开始编写代码。 第一步是为文本字段添加一个插座

@IBOutlet weak var textFieldBottomConstraint: NSLayoutConstraint! 

并将其与约束“底部空间到:Superview”相关联。有必要将文本字段的位置调整到屏幕底部。 当键盘出现时,我们将该变量的值更改为键盘高度,而当键盘隐藏时,我们将返回至原始零值。

下一步是添加隐藏键盘的功能。 我们将提供两种方法来执行此操作:

  1. 在文本框外点击子控制器的内容。 在这种情况下,为深灰色区域。
  2. 按键盘上的Return键。

要实现第一个选项,请将以下代码添加到viewDidLoad处理程序中

 let tapGestureRecognizer = UITapGestureRecognizer(target: self, 
action: #selector(viewTapHandler(recognizer:)))
view.addGestureRecognizer(tapGestureRecognizer)

并实现如下的viewTapHandler方法

 func viewTapHandler(recognizer: UITapGestureRecognizer) { 
view.endEditing(true)
}

在此代码中,我们将抽头检测添加到控制器的根视图。 检测到轻敲后,我们将关闭视图及其子视图的编辑模式,因此文本字段不再是第一响应者,并且键盘也消失了。

为了实现第二个选项,我们必须将控制器转换为文本字段委托并实现textFieldShouldReturn委托方法

 extension ChildViewController: UITextFieldDelegate { 
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return textField.resignFirstResponder()
}
}

在textFieldShouldReturn方法中,我们在文本字段上退出第一响应者状态。 现在,我们需要返回Main.storyboard,并使ChildViewController成为文本字段的委托。 实现子控制器的下一个阶段是处理系统键盘的出现和消失事件。 为此,我们将重新定义viewWillAppear方法并订阅键盘的出现和消失事件。

 override func viewWillAppear(_ animated: Bool) { 
super.viewWillAppear(animated)

NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: .UIKeyboardWillShow,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide(notification:)),
name: .UIKeyboardWillHide,
object: nil)
}

如果我们订阅系统事件,则必须提供一种取消订阅这些事件的方法,因此我们重新定义viewDidDisappear方法并将自己从观察者的键盘事件中删除。

 override func viewDidDisappear(_ animated: Bool) { 
super.viewDidDisappear(animated)

NotificationCenter.default.removeObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: .UIKeyboardWillShow,
object: nil)
  NotificationCenter.default.removeObserver(self, 
selector: #selector(keyboardWillHide(notification:)),
name: .UIKeyboardWillHide,
object: nil)
}

现在,当键盘出现时,将调用我们的keyboardWillShow方法。 当它消失时,将调用keyboardWillHide方法。 我们还没有这些方法,因此我们必须添加它们,但是现在让我们将它们留空。

 func keyboardWillShow(notification: NSNotification) { 
}

func keyboardWillHide(notification: NSNotification) {
}

在这两种方法中,都将发送NSNotification类对象。 从这个对象,我们需要获取键盘的大小以及速度和动画参数。 我们将使用此信息将子控制器扩展到全屏并初始化输入字段,以便它不会被键盘阻止。

父控制器负责扩展子控制器。 由于我们决定使用委托来解决此问题,因此必须使父控制器成为孩子的委托。 然后,子控制器应该能够将键盘的出现或消失通知父母。 为此,我们将以下协议添加到子控制器:

 protocol ChildViewControllerKeyboardDelegate: class { 
func keyboardWillShow(withFrame frame: CGRect,
animationDuration: TimeInterval,
options: UIViewAnimationOptions)
func keyboardWillHide(withFrame frame: CGRect,
animationDuration: TimeInterval,
options: UIViewAnimationOptions)
}

和以下属性,它们将是子控制器委托

 weak var keyboardDelegate: ChildViewControllerKeyboardDelegate? 

现在我们有了一个委托,让我们实现一个将从NSNotification对象获取参数的方法,并使用这些参数调用委托方法。 我们还将这些参数用于文本字段动画。 我们将从我们的keyboardWillShow和keyboardWillHide方法中调用此方法,还将把打开和隐藏键盘的信息传递给它。 因此,用于处理键盘事件的代码应如下所示:

 func keyboardWillShow(notification: NSNotification) { 
keyboardWill(show: true, keyboardNotification: notification)
}

func keyboardWillHide(notification: NSNotification) {
keyboardWill(show: false, keyboardNotification: notification)
}

func keyboardWill(show isShow: Bool,
keyboardNotification notification: NSNotification) {
if let userInfo = notification.userInfo {
var animationDuration: TimeInterval = 0
if let duration =
userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber {
animationDuration = duration.doubleValue
}
var keyboardEndFrame: CGRect = CGRect.zero
if let frameEnd =
userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue {
keyboardEndFrame = frameEnd.cgRectValue
}
var animationCurve: UInt = 0
if let curve =
userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
animationCurve = curve.uintValue
}

// convert UIViewAnimationCurve to UIViewAnimationOptions
// (see UIViewAnimationOptions help)
let animationOptionCurve = UIViewAnimationOptions(
rawValue: animationCurve << 16)
let options: UIViewAnimationOptions = [
UIViewAnimationOptions.beginFromCurrentState,
animationOptionCurve]

if isShow {
keyboardDelegate?.keyboardWillShow(
withFrame: keyboardEndFrame,
animationDuration: animationDuration,
options: options)
}
else {
keyboardDelegate?.keyboardWillHide(
withFrame: keyboardEndFrame,
animationDuration: animationDuration,
options: options)
}

textFieldBottomConstraint.constant =
(!isShow) ? 0 : keyboardEndFrame.height
UIView.animate(withDuration: animationDuration,
delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
}
}

让我们仔细看一下方法keyboardWill(显示isShow:Bool,keyboardNotification通知:NSNotification)。 在方法开始时,我们从通知中获取有关动画参数和键盘大小的信息。 根据文档,为了从UIViewAnimationCurve类型的变量中获取UIViewAnimationOptions类型的对应值,我们需要进行左移16位。当拥有这些参数时,我们可以调用适当的委托方法并进行更改文本输入字段的缩进到所需值。 在方法的最后,我们对子视图进行了动画重排,以获取文本输入字段的动画运动。

主控制器委托的实现

现在,让我们切换到ContainerViewController类并实现其ChildViewControllerKeyboardDelegate协议。 为此,我们创建以下类扩展

 extension ContainerViewController: ChildViewControllerKeyboardDelegate { 
func keyboardWillShow(withFrame frame: CGRect,
animationDuration: TimeInterval,
options: UIViewAnimationOptions) {
}

func keyboardWillHide(withFrame frame: CGRect,
animationDuration: TimeInterval,
options: UIViewAnimationOptions) {
}
}

当前,协议方法为空,我们必须在其中实现所附加控制器大小的更改。 为此,为Content View对象及其顶部空间添加两个Outlet:Superview约束,并将它们与Main.storyboard中的相关对象相关联:

 @IBOutlet weak var contentView: UIView! 
@IBOutlet weak var contentViewTopConstraint: NSLayoutConstraint!

切换到Main.storyboard并为连接主控制器的segue命名为OpenChildSegue

并在ContainerViewController类中重新定义方法prepare(对于segue:UIStoryboardSegue,sender:?Any),如下所示:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 
if segue.identifier == "OpenChildSegue" {
if let childController = segue.destination as? ChildViewController {
childController.keyboardDelegate = self
}
}
}

在这里,我们将ContainerViewController分配为附加的ChildViewController的委托。 现在,让我们回到协议方法并按如下方式实现它们:

 func keyboardWillShow(withFrame frame: CGRect, 
animationDuration: TimeInterval,
options: UIViewAnimationOptions) {
contentViewTopConstraint.constant = -contentView.frame.height
UIView.animate(withDuration: animationDuration,
delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
}

func keyboardWillHide(withFrame frame: CGRect,
animationDuration: TimeInterval,
options: UIViewAnimationOptions) {
contentViewTopConstraint.constant = 0
UIView.animate(withDuration: animationDuration,
delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
}

当键盘出现时,我们将更改“内容视图”布局,使其位于屏幕上方,并且由于我们连接的控制器已绑定到“内容视图”,因此它会自动扩展至屏幕顶部。 当键盘消失时,我们将Content View返回其原始位置,从而将连接的控制器缩小为原始尺寸。

在模拟器或设备上运行项目,您应该看到以下行为:

摘要

最后,我们使用委托模式和自动布局技术解决了该问题。 使用以下链接下载项目的源代码。