使用系统键盘时的错误

与应用程序进行交互时,我们有时会激活系统键盘以键入消息或填写必填字段。 您是否在显示键盘时遇到过这样的情况,但是没有用于输入消息的字段,反之亦然-那里的键盘不可见输入文本的位置? 错误可能与特定应用程序中的问题以及系统键盘的一般缺陷有关。

Mail.ru的iOS开发人员 Konstantin Mordan 看到了他的所有工作:在分析了iOS中的键盘控制方法后,他决定分享他用来检测和修复它们的主要错误和方法。



警告:在剪切下,我们放置了许多gif以便清楚地演示错误。 在康士坦汀在AppsConf上的视频报告中,您将找到更多示例。

实施系统键盘调用


首先,我们首先了解如何实现键盘调用。

假设您正在开发一个应用程序,其任务是使用键盘将Aika(South Park字符)组装成整个加拿大人。 当Aiku压在肚子上时,键盘会离开,从而将英雄的腿抬到头部。

要实现此任务,可以使用InputAccessoryView或处理系统通知。

InputAccessoryView


让我们看一下第一个选项。

在ViewController中,创建一个将随键盘一起上升的View,并为其提供一个框架。 重要的是,不应将此视图添加为子视图。 接下来,我们覆盖canBecomeFirstResponder属性并返回true。 在我们重新定义UIResponder属性-inputAccessoryView并将View放在那里之后。 要关闭键盘,请添加tapGesture并在其处理程序中, 重置我们创建 View 的firstResponder

class ViewController: UIViewController { var tummyView: UIView { let frame = CGRect(x: x, y: y, width: width, height: height) let v = TummyView(frame: frame) return v } override var canBecomeFirstResponder: Bool { return true } override var input AccessoryView: UIView? { return tummyView } func tapHandler ( ) { tummyView.resignFirstResponder ( ) } } 

任务已完成,系统本身处理键盘状态更改,显示它并引发取决于它的View。



系统通知处理


在处理通知的情况下,我们必须自己处理以下几组的通知:

  • 何时将显示键盘:keyboardWillShowNotification,keyboardDidShowNotification;
  • 当键盘将/被隐藏时:keyboardWillHideNotification,keyboardDidHideNotification;
  • 何时更改键盘框架:keyboardWilChangeFrameNotification,keyboardDidChangeFrameNotification。

为了实现这种情况,让我们以keyboardWilChangeFrameNotification为例 ,因为该通知是在显示键盘和隐藏键盘时发送的。

我们创建一个keyboardTracker,在其中我们订阅一个KeyboardWillChangeFrame通知,并在处理程序中获取键盘框架,将其从屏幕坐标系转换为窗口坐标系,计算键盘高度并将View应该由键盘提升的View的Y值更改为该高度。

 class KeyboardTracker { func enable ( ) { notificationCenter.add0observer(self, seletor: #selector( keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } func keyboardWillChangeFrame ( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo [ UIResponder.keyboardFrameEndUserInfoKey ] as! NSValue ) .cgRectValue let keyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil ) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY delegate.keyboardWillChange ( keyboardHeight ) } } 

至此,我们的任务完成,键盘抬起,将艾克收集到加拿大。

如我们所见,在两种情况下,键盘的实现都很容易,因此每个人都可以自行选择合适的方法。 在我们的项目中,我们做出了选择通知的选择,因此,进一步的示例和见解将与通知的处理相关联。

寻找错误


如果调用键盘的方法是如此简单,那么这些错误是从哪里来的呢? 当然,如果应用程序仅复制用于打开和关闭键盘的脚本,则不会有问题。 但是,如果您改变了通常的做法,请记住,不仅我们的应用程序可以使用键盘,而且其他用户也可以使用键盘,并且用户也可以在它们之间进行切换,所以无法避免意外。

让我们来看一个例子。 为此,将我们的应用程序与Ike一起使用:打开键盘,切换到Notes,打印内容并返回到该应用程序。



哪些问题已经可见? 首先,App Switcher中没有键盘,尽管当您最小化应用程序时,它是可见的,而不是其他内容可见。 其次,当您返回该应用程序时,键盘仍然不存在,并且Ike的双腿从屏幕上掉下来。

让我们看一下这种现象的原因。 我们都从应用程序的生命周期图中记住,应用程序从活动状态到不活动状态的转换首先要在前台,然后在后台进行,这需要时间。

键盘的生命周期如何? 在iOS中,对于每个时间单位,键盘只能由一个正在运行的应用程序拥有,但是在其上签名的所有应用程序都会收到有关键盘状态更改的通知。

从一个应用程序切换到另一个应用程序时,系统会重置其firstResponder,该触发器充当隐藏键盘的触发器。 系统首先发送KeyboardWillHide通知以使键盘消失,然后发送keyboardDidHideNotification。 通知将转到第二个应用程序。 在新应用程序中,我们打开键盘:系统发送keyboardWillShowNotification以便键盘出现,然后发送keyboardDidShowNotification-一个演示 ,其中包含循环的各个阶段。



如果查看该报告的一部分 (从8:39开始),您将看到隐藏键盘后系统发送keyboardDidHideNotification来将第一个应用程序置于非活动状态的瞬间。 当您切换到运动应用程序并启动键盘时,系统将发送keyboardWillShowNotification。 但是由于切换和启动过程很快,并且生命周期各阶段之间的过渡时间会更长,因此收到的通知不仅会处理体育运动的申请,还会处理尚未进入后台的啤酒的申请。

在找出原因之后,现在让我们找到解决Ike问题的方法。

错误的决定


首先想到的是在通过启用/禁用KeyboardTracker最小化/扩展应用程序时取消订阅/订阅通知的想法。

对于取消订阅,我们使用applicationWillResignActive方法或系统中类似的通知处理程序;对于订阅,我们使用applicationDidBecomeActive,但是为了不丢失任何内容,我们还将通知applicationWillEnterForeground方法,该方法在应用程序进入前台但尚未激活时被调用。

当您在应用程序中启动键盘时,很可能一切都会成功,但是如果进行更复杂的测试(例如,打开键盘并尝试记录语音拨号),该解决方案将无法正常工作。



发生什么事了 单击语音消息拨号按钮后,将重置应用程序firstResponder,关闭键盘,调用applicationWillResignActive方法,然后我们取消订阅。 关闭警报后,系统恢复了应用程序状态,但是在调用applicationWillEnterForeground方法(尤其是applicationDidBecomeActive)之前。

好的决定


另一种解决方案是使用保护性肉汤(布尔)。

 var wasTummyViewFirstResponderBeforeApp0idEnterBackground func willResignActive( notification: NSNotification) { wasTextFieldFirstResponderBeforeAppDidEnterBackground = tummyView.isFirstResponder } func willEnterForeground ( notification: NSNotification) { if wasTextFieldFirstResponderBeforeAppDidEnterBackground { UIView.performWithourAnimation { tummyView.becomeFirstResponder ( ) } } } 

我们记得在主题之前是否打开了键盘,应用程序如何停止处于活动状态,并且在applicationWillEnterForeground方法中我们恢复了先前的状态。 唯一需要修复的是应用程序切换器中的漏洞。



应用切换器


应用程序切换器显示应用程序进入后台后系统执行的应用程序快照。 屏幕快照显示,我们的应用程序的快照是在另一个应用程序已经在使用键盘时制作的。 这并不重要,但只需单击几下即可对其进行修复。

好的解决方案


可以从已经学会隐藏敏感数据的银行应用程序中借用该解决方案,也可以从Apple那里读取该解决方案。

您可以在applicationDidEnterBackground方法中隐藏数据,模糊并显示初始屏幕,然后在applicationWillEnterForeground方法中返回到通常的视图层次结构。

此选项不适合我们,因为在调用applicationDidEnterBackground方法时,我们的应用程序不再具有键盘。

好的决定


我们将使用熟悉的方法willResignActive,willEnterForeground和didBecomeActive。

尽管我们的应用程序还具有键盘,但是您将需要在willResignActive方法中创建自己的应用程序快照并将其放入层次结构中。

 func willResignActive( notificaton: NSNotification) { let keyWindow = UIApplication.shared.keyWindow imageView = UIImageView( frame: keyWindow.bounds) imageView.image = snapshot ( ) let lastSubview = keyWindow.subviews.last lastSubview( imageView) } 

在willEnterForeground和didBecomeActive方法中,我们还原视图层次结构并删除快照。

 func willEnterForeground( notification: NSNotification) { imageView.removeFromSuperview( ) } func didBecomeActive( notification: NSNotification) { imageView.removeFromSuperview( ) } 

结果,我们修复了以下两种情况:在应用切换器中,精美的​​画面和切换时键盘不再跳动。 这些似乎不是那么重要,但是对于产品开发而言,这些要点非常重要。

坏消息


我们成功解决Ike问题的方法是在最小化应用程序之前打开键盘的情况。 如果在不扩展键盘的情况下进行切换,那么我们将再次看到Ike的支脚跌落到下方。



这不仅是我们应用程序的问题,在使用通知的Facebook上,甚至在使用inputAccessoryView来控制键盘的iMessage上,也都观察到了此行为。 这是由于以下事实:在切换到后台之前,应用程序设法处理其他人的键盘通知。

交互式键盘关闭


使用Ike向我们的应用程序添加一些功能,以指导程序交互式地隐藏键盘。



错误的决定


实现此功能的一种方法是更改​​键盘视图的框架。 我们创建panGestureRecognizer,在其处理程序中,根据手指的位置,计算键盘Y坐标的新值,找到键盘视图并用Y坐标值进行更新。

 func panGestureHandler( ) { let yPosition: CGFloat = value keyboardView( )?.frame.origin.y = yPosition } 

键盘显示在一个单独的窗口中,因此您需要遍历应用程序中的整个窗口阵列,检查该阵列的每个元素是否是键盘窗口,如果是,则从中获取显示键盘的视图。

 func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view } 

不幸的是,此解决方案无法在iPhone X及更高版本的iPhone X上正常工作,因为当您移动手指时,您可以轻轻触摸下方的指示器,这可以最大程度地减少应用程序的运行。 此后,交互式隐藏停止工作。



问题出在窗口阵列中。



手势完成后,系统会在现有的键盘窗口上方创建一个新的键盘窗口。 这是不可想象的,但却是事实。 结果,事实证明该数组包含两个具有相同坐标的键盘窗口,但第一个是隐藏的。



事实证明,遍历窗口阵列,我们发现第一个满足条件的窗口,尽管它是隐藏的,但仍开始使用它。

如何解决? 旋转窗口阵列。

 func panGeastureHandler( ) { let yPosition: CGFloat = 0.0 keyboardView( )?.frame.origin.y = yPosition } func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows.reversed( ) let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view } 

iPad上的键盘功能


iPad上的键盘除了通常的状态外,还处于非对接状态。 用户可以在屏幕上移动它,将它分为两​​个部分,甚至可以以滑行模式(在另一个之上)启动应用程序。 当然,重要的是,在所有这些模式下,键盘都能正常运行而不会出现错误。

让我们检查一下我们的Hayke。



las,现在不是这种情况。 用户开始在屏幕上移动键盘后,Ike的腿从头顶飞过,直到下一次打开键盘后才出现在适当的位置。 让我们尝试使用分离式键盘将其修复。

原因


让我们从分析通知开始。 单击拆分按钮后,我们得到两组通知-keyboardWillChangeFrameNotification,keyboardWillHideNotification,keyboardDidChangeFrameNotification,keyboardDidHideNotification。 组之间的区别仅在于键盘的坐标。

当我们单击拆分按钮时,键盘会减小,并且第一组通知到达。 当键盘拆分并上升时-我们收到了第二包通知。

重要的是,我们会收到有关键盘消失的通知,但不会收到显示键盘的通知。 顺便说一句,这是支持使用keyboardWillChangeFrameNotification的另一个优点。

那么,为什么在我们开始在屏幕上移动键盘后,艾克的腿就会飞走?

此时,系统会向我们发送一个KeyboardWillChangeFrameNotification,但其中的坐标为(0.0、0.0、0.0、0.0),因为系统不知道移动完成后键盘将位于哪一点。

如果我们在处理键盘框架变化的当前代码中替换零,那么事实证明键盘的高度等于窗口的高度。 这就是Ike的腿从屏幕上飞出的原因。

好的决定


要解决我们的问题,首先,我们将学习了解键盘何时处于非固定模式,并且用户可以在屏幕上移动键盘。

为此,只需比较窗口和maxY键盘的高度。 如果它们相等,则键盘处于其正常状态;如果maxY小于窗口的高度,则用户移动键盘。 结果,以下代码出现在keyboardTracker中:

 class KeyboardTracker { func enable( ) { notificationCenter.addObserver( self, selector:#selector( keyboardWillChangeFrame), name:UIResponder.keyboardWillChangeFrameNotification, object:nil) } func keyboardWillChangeFrame( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue let leyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY let isKeyboardUnlocked = isIPad ( ) && keyboardFrame/maxY < windowHeight if isKeyboardUnlocked { keyboardHeight = 0.0 } delegate.keyboardWillChange ( keyboardHeight) } } 

我们自定义将高度设置为零,现在随着键盘的移动,Ike的腿下降并固定在那里。



唯一剩下的误解是,当拆分键盘时,Ike的腿不会立即掉下。 如何解决?

我们将教导keyboardTracker不仅可以与keyboardWillChangeFrameNotification一起使用,而且还可以与keyboardDidChangeFrame一起使用。 您不必编写新代码,只需添加一下这是iPad的复选框,以免进行不必要的计算。

 class KeyboardTracker { func keyboardDidChangeFrame( notification: NSNotification) { if isIPad ( ) == false { return } 




如何检测错误?


丰富的日志


在我们的项目中,日志以以下格式编写:在方括号中,日志所属的模块和子模块的名称,然后是日志本身的文本。 例如,像这样:
[keyboard][tracker] keyboardWillChangeFrame: calculated height - 437.9

在代码中,它看起来如下-使用顶级标签创建一个记录器,并将其传输到跟踪器。 在跟踪器内部,从记录器中分离出带有二级标签的记录器,该记录器用于在类内部进行记录。

 class KeyboardTracker { init(with logger: Logger) { self.trackerLogger = logger.dequeue(withTag: "[tracker]") } func keyboardWillChangeFrame(notification: NSNotification) { let height = 0.0 trackerLogger.debug("\(#function): calculated height - \(height)") } } 

所以我保证了整个keyboardTracker,这很好。 如果测试人员发现问题,我将获取日志文件,并准确查找框架不适合的位置。 这花费了太多时间,因此,除了日志记录之外,其他方法也开始应用。

看门狗


在我们的项目中,看门狗用于优化UI流。 德米特里·库金(Dmitry Kurkin)在过去的AppsConf大会上告诉了这一点

看门狗是监视另一个进程或线程的进程或线程。 此机制使您可以监视键盘的状态以及依赖于键盘的视图并报告问题。

为了实现这种功能,我们创建了一个计时器,该计时器每秒将用Hayk的腿检查视图的正确位置,如果错误则将其记录下来。

 class Watchdog { var timer: Timer? func start ( ) { timer = Timer ( timeInterval: 1.0, repeats: true, block: { ( timer ) in self.woof ( ) } ) } } 

顺便说一句,您不仅可以记录最终结果,还可以记录中间计算。

结果,大量的日志记录+看门狗提供了有关该问题,键盘状态的准确数据,并减少了修复错误的时间,但是对于必须忍受错误直到下一个版本的beta用户而言,它几乎没有帮助。

但是,如果看门狗不仅可以发现问题,而且可以解决问题,该怎么办?

在看门狗得出结论认为视图的坐标不会收敛的代码中,我们添加了fixTummyPosition方法并自动将坐标放置在适当的位置。

使用此选项,我的日志中会积累很多有用的信息,并且用户根本不会注意到视觉问题。 这似乎很棒,但是现在我找不到有关键盘的任何问题。

当检测到错误时,它有助于向看门狗方法中添加生成测试缓存的功能。 当然,此代码将添加到remout配置下。

现在,在下一个发行版之后,您可以打开测试崩溃生成功能,并且如果用户遇到键盘问题,则其应用程序崩溃,并且由于收集了日志,您可以修复错误。

仪表板


我们介绍的最后一个技巧是在wahtchdog记录统计信息的同时发送统计信息。 根据获得的数据,我们绘制了检测到的错误数量,并且在第一次迭代之后,操作数量减少了四倍。 当然,不可能将问题减少到零,但是用户的主要抱怨不再了。

下周, Saint AppsConf将在圣彼得堡举行,在那里您不仅可以向康斯坦丁(Konstantin)提问,还可以向许多iOS领域的演讲者提问。

Source: https://habr.com/ru/post/zh-CN462515/


All Articles