实现方法有
修改响应链(推荐)
遵循 UIKeyInput 协议
自定义 Menu controller
前两种方法的代码已上传 GitHub:https://github.com/Silence-GitHub/MenuControllerDemo
第 3 种方法的 GitHub 链接:https://github.com/Silence-GitHub/SWMenuController
在此之前,介绍 UIMenuController 的使用方法,以及键盘会隐藏的原因。
如果只要实现功能,看第 1 种方法的代码就可以,正文基本不用看。如果要理解响应链(Responder chain)相关的原理,先看 Apple 的文档 Understanding Responders and the Responder Chain
自定义一个需要显示 UIMenuController 的视图,以 UIButton 为例,自定义类 ShowMenuButton
ShowMenuButton 必须重载 canBecomeFirstResponder 属性,返回 true 才能显示菜单(UIMenuController)。第一响应者(First responder)才能处理菜单,如果 canBecomeFirstResponder 返回 false,不能成为第一响应者,菜单不会显示。
重载 canPerformAction(_:withSender:) 方法,过滤需要显示的菜单按钮(UIMenuItem)。参数 action 有 copy(_:)、paste(_:) 等 UIResponderStandardEditActions 协议的方法。对需要进行的操作返回 true,显示菜单按钮(以上代码显示“Copy”菜单按钮);对不需要的操作返回 false,尝试隐藏菜单按钮(菜单按钮不一定隐藏,如果响应链中有其他响应者返回 true,此菜单按钮仍然会显示)。此方法在默认情况下(没有实现此方法的时候),如果当前类实现了相应的 action,就会返回 true;如果没有实现相应的 action,则调用下一个响应者的此方法。如果不实现此方法(或此方法返回 false),响应链上有响应者也没实现此方法(或此方法返回 true)但实现了 copy(_:) 方法,则“Copy”菜单按钮会显示。建议实现此方法,至少在响应链的这一层控制菜单按钮。
实现与需要显示的菜单按钮对应的 action 方法,以上代码为 copy(_:) 方法。当菜单按钮被点击,action 方法会被发送。如果没有实现 canPerformAction(_:withSender:) 方法,UIKit 会沿着响应链寻找实现 action 的响应者,把 action 方法发给实现 action 的响应者。一旦实现了 canPerformAction(_:withSender:) 方法且返回 true,action 方法就会发送给当前响应者,不会沿着响应链去找实现 action 的响应者,所以必须实现相应的 action 方法。
在控制器(UIViewController)中,让自定义的 ShowMenuButton 监听点击事件
点击 button 弹出菜单
在使用 UIMenuController 之前,使 button 成为第一响应者,菜单才能显示。
控制器没有实现 canPerformAction(_:withSender:) 方法,实现了 customItemDidSelect,从 button 开始沿着响应链可以找到当前控制器,因此自定义菜单按钮可以显示。如果控制器实现 canPerformAction(_:withSender:) 方法且返回 false,则自定义菜单按钮不会显示。
如有需要,隐藏菜单
注意,UIMenuController 只有一个实例,隐藏后 menuItems 还保留显示时的值,下次在其他地方显示还会出现旧的自定义菜单按钮,因此要在适当的时候更新 menuItems 属性。
UITextView、UITextField 成为第一响应者(点击输入框,准备输入),键盘会显示。输入框不是第一响应者,键盘会隐藏。由于要显示菜单的自定义控件调用 becomeFirstResponder() 方法,成为第一响应者,则输入框就不是第一响应者,所以键盘隐藏。
这是目前最好的方法,代码量最少。可以正常使用 UIMenuController,并且键盘能正常显示、输入,输入框的光标仍然闪烁。
方法思路来自:http://stackoverflow.com/questions/13601643/uimenucontroller-hides-the-keyboard
然而,那些代码还有 bug,这里会解决。既然输入框失去第一响应者,键盘会隐藏,那就让输入框保持第一响应者。通过改变响应链,让菜单事件传递给能处理的响应者。
以 UITextView 为例,自定义类 CustomResponderTextView
重载 next 属性,改变响应链。重载 canPerformAction(_:withSender:) 方法,在响应链改变时都返回 false。
控制器的代码需要修改
如果 text view 不是第一响应者,键盘没显示,和原来一样。如果 text view 是第一响应者,改变响应链,让输入框的下一个响应者(next)成为 button。菜单要显示哪些按钮,从第一响应者 text view 开始,沿着响应链,通过 canPerformAction(_:withSender:) 方法判断。虽然 text view 的 canPerformAction(_:withSender:) 方法返回 false,但 button 的 canPerformAction(_:withSender:) 方法对 copy(_:) 方法返回 true,所以会显示“Copy”菜单按钮。点击“Copy”菜单按钮,button会执行 copy(_:) 方法。控制器也在这条响应链上,实现了 customItemDidSelect 方法,没实现 canPerformAction(_:withSender:) 方法,则 canPerformAction(_:withSender:) 方法默认对 customItemDidSelect 方法返回 true,所以会显示自定义菜单按钮。点击自定义菜单按钮,控制器会执行 customItemDidSelect 方法。
监听菜单消失,在将要消失时,恢复响应链,清除自定义菜单按钮,移除通知监听。
输入框自己也可以显示菜单。如果先点击 button,然后点击 text view,让 text view 显示菜单,自定义菜单按钮仍然显示。因为还没有监听菜单消失,所以没有清除自定义菜单按钮。因此,监听键盘显示
在键盘将要显示时清除自定义菜单按钮,在控制器释放前移除通知监听
这个方法一定会显示键盘,不能隐藏键盘。同时,输入框的光标不闪烁。一般情况下能正常输入,但系统中文输入法只响应部分按键(回车、空格等)。
方法思路来自:http://stackoverflow.com/questions/4282964/becomefirstresponder-without-hiding-keyboard/4284675#4284675
在 GitHub 上也有这个方法的代码示例:https://github.com/jaredsinclair/UIMenuControllerTest
虽然这里会修复那些代码的 bug,但输入框光标不闪烁等问题依然存在。遵循 UIKeyInput 协议的 UIResponder 成为第一响应者,键盘就会弹出。
以 UIButton 为例,自定义类 KeyInputButton
UIKeyInput 协议的方法与键盘输入相关。hasText 方法表示有没有文本。deleteBackward 方法当键盘的删除键点击时调用。insertText(_:) 方法在键盘输入时调用。让控制器成为 button 的 delegate,把这些方法传给 text view (UITextView,不用自定义)
点击显示菜单
由于 button 成为第一响应者时键盘一定会显示,所以每次都可以让 button 调用 becomeFirstResponder 方法。
依然要监听菜单消失,清除自定义菜单按钮,移除通知监听。
需要注意的是,UIMenuController 的 setMenuVisible(_:animated:) 方法要延迟调用,否则菜单可能刚出现就消失。
由于之前尝试其他方法不满意(当时修改响应链的方法还有问题),于是查找自定义的菜单。找到一个:https://github.com/camelcc/MenuPopOverView
自己也写了一个:https://github.com/Silence-GitHub/SWMenuController
以下介绍自己写的 SWMenuController,先看效果图
基本够用,但是和 UIMenuController 还是有差距(例如动画效果、自动调整字体大小等)。
实现原理是,继承 UIView,添加 UIButton 作为菜单按钮,添加到 window 来显示。
与 UIMenuController 相似,但所有菜单按钮都要自定义,传入菜单按钮标题的数组
实现 SWMenuControllerDelegate 方法,处理第 index 个菜单按钮的点击事件(index 从 0 开始)
func menuController(_ menu: SWMenuController, didSelected index: Int) { print(menu.menuItems[index]) // Do something for menu at index}
本文转自 sshpp 51CTO博客,原文链接:http://blog.51cto.com/12902932/1926244,如需转载请自行联系原作者