开发者社区> 技术小甜> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

iOS 使用 UIMenuController 且不隐藏键盘的方法

简介:
+关注继续查看

在键盘显示的时候使用 UIMenuController 弹出菜单,保持键盘显示且可输入的状态。

1089786-20170508115925644-887953559.png

实现方法有

  1. 修改响应链(推荐)

  2. 遵循 UIKeyInput 协议

  3. 自定义 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 的使用方法

自定义一个需要显示 UIMenuController 的视图,以 UIButton 为例,自定义类 ShowMenuButton

class ShowMenuButton: UIButton {    // Return true so that menu controller can display
    override var canBecomeFirstResponder: Bool { return true }    
    // Return true to show menu for given action
    // Action is in UIResponderStandardEditActions
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {        return action == #selector(copy(_:))
    }    
    override func copy(_ sender: Any?) {        print(#function)
    }
}

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.addTarget(self, action: #selector(showMenuButtonClicked(_:)), for: .touchUpInside)

点击 button 弹出菜单

@objc private func showMenuButtonClicked(_ button: UIButton) {    // Let button become first responder so that menu can display
    button.becomeFirstResponder()    // Only one UIMenuController instance
    let menu = UIMenuController.shared    // Custom menu item can perform custom action
    let customItem = UIMenuItem(title: "Custom item", action: #selector(customItemDidSelect))    // Set custom menu item
    menu.menuItems = [customItem]    // Sets the area in a view above or below which the editing menu is positioned
    menu.setTargetRect(button.frame, in: view)    // Show menu
    menu.setMenuVisible(true, animated: true)
}// Custom menu item actionfunc customItemDidSelect() {    print(#function)
}

在使用 UIMenuController 之前,使 button 成为第一响应者,菜单才能显示。

控制器没有实现 canPerformAction(_:withSender:) 方法,实现了 customItemDidSelect,从 button 开始沿着响应链可以找到当前控制器,因此自定义菜单按钮可以显示。如果控制器实现 canPerformAction(_:withSender:) 方法且返回 false,则自定义菜单按钮不会显示。

如有需要,隐藏菜单

UIMenuController.shared.setMenuVisible(false, animated: true)

注意,UIMenuController 只有一个实例,隐藏后 menuItems 还保留显示时的值,下次在其他地方显示还会出现旧的自定义菜单按钮,因此要在适当的时候更新 menuItems 属性。

UITextView、UITextField 成为第一响应者(点击输入框,准备输入),键盘会显示。输入框不是第一响应者,键盘会隐藏。由于要显示菜单的自定义控件调用 becomeFirstResponder() 方法,成为第一响应者,则输入框就不是第一响应者,所以键盘隐藏。

不隐藏键盘的方法

修改响应链(推荐)

这是目前最好的方法,代码量最少。可以正常使用 UIMenuController,并且键盘能正常显示、输入,输入框的光标仍然闪烁。

方法思路来自:http://stackoverflow.com/questions/13601643/uimenucontroller-hides-the-keyboard
然而,那些代码还有 bug,这里会解决。既然输入框失去第一响应者,键盘会隐藏,那就让输入框保持第一响应者。通过改变响应链,让菜单事件传递给能处理的响应者。

以 UITextView 为例,自定义类 CustomResponderTextView

class CustomResponderTextView: UITextView {    weak var overrideNext: UIResponder?    
    override var next: UIResponder? {        if let responder = overrideNext { return responder }        return super.next
    }    
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {        if overrideNext != nil { return false }        return super.canPerformAction(action, withSender: sender)
    }
}

重载 next 属性,改变响应链。重载 canPerformAction(_:withSender:) 方法,在响应链改变时都返回 false。

控制器的代码需要修改

// Init text view when view did loadvar textView: CustomResponderTextView!@objc private func showMenuButtonClicked(_ button: UIButton) {    if textView.isFirstResponder {        // Change responder chain
        textView.overrideNext = button        // Observe "will hide" to do some cleanup
        // Do not use "did hide" which is not fast enough
        NotificationCenter.default.addObserver(self, selector: #selector(menuControllerWillHide), name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
    } else {
        button.becomeFirstResponder()
    }    let menu = UIMenuController.shared    let customItem = UIMenuItem(title: "Custom item", action: #selector(customItemDidSelect))
    menu.menuItems = [customItem]
    menu.setTargetRect(button.frame, in: view)
    menu.setMenuVisible(true, animated: true)
}    
func customItemDidSelect() {    print(#function)
}    
@objc private func menuControllerWillHide() {    // Change responder chain back
    textView.overrideNext = nil
    // Prevent custom menu items from displaying in text view
    UIMenuController.shared.menuItems = nil
    // Remove notification observer
    NotificationCenter.default.removeObserver(self, name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
}

如果 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 显示菜单,自定义菜单按钮仍然显示。因为还没有监听菜单消失,所以没有清除自定义菜单按钮。因此,监听键盘显示

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: Notification.Name.UIKeyboardWillShow, object: nil)

在键盘将要显示时清除自定义菜单按钮,在控制器释放前移除通知监听

@objc private func keyboardWillShow() {    // Prevent custom menu item from displaying in text view
    UIMenuController.shared.menuItems = nil}deinit {    NotificationCenter.default.removeObserver(self)
}

遵循 UIKeyInput 协议

这个方法一定会显示键盘,不能隐藏键盘。同时,输入框的光标不闪烁。一般情况下能正常输入,但系统中文输入法只响应部分按键(回车、空格等)。

方法思路来自:http://stackoverflow.com/questions/4282964/becomefirstresponder-without-hiding-keyboard/4284675#4284675
在 GitHub 上也有这个方法的代码示例:https://github.com/jaredsinclair/UIMenuControllerTest
虽然这里会修复那些代码的 bug,但输入框光标不闪烁等问题依然存在。遵循 UIKeyInput 协议的 UIResponder 成为第一响应者,键盘就会弹出。

以 UIButton 为例,自定义类 KeyInputButton

protocol KeyInputButtonDelegate: class {    func keyInputButtonHasText(_ button: KeyInputButton) -> Bool
    func keyInputButton(_ button: KeyInputButton, didInsertText text: String)
    func keyInputButtonDidDeleteBackward(_ button: KeyInputButton)}class KeyInputButton: UIButton, UIKeyInput {    // Return true so that menu controller can display
    override var canBecomeFirstResponder: Bool { return true }    
    // Return true to show menu for given action
    // Action is in UIResponderStandardEditActions
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {        return action == #selector(copy(_:))
    }    
    override func copy(_ sender: Any?) {        print(#function)
    }    
    // MARK: - UIKeyInput
    
    weak var delegate: KeyInputButtonDelegate?    
    var hasText: Bool {        if let d = delegate {            return d.keyInputButtonHasText(self)
        }        return false
    }    
    // SOGOU, system English, system emoji input method work
    // System Chinese input method typing some characters dose not call this method (but some characters call, e.g "\n" and " ")
    func insertText(_ text: String) {
        delegate?.keyInputButton(self, didInsertText: text)
    }    
    func deleteBackward() {
        delegate?.keyInputButtonDidDeleteBackward(self)
    }
}

UIKeyInput 协议的方法与键盘输入相关。hasText 方法表示有没有文本。deleteBackward 方法当键盘的删除键点击时调用。insertText(_:) 方法在键盘输入时调用。让控制器成为 button 的 delegate,把这些方法传给 text view (UITextView,不用自定义)

func keyInputButtonHasText(_ button: KeyInputButton) -> Bool {    return textView.hasText
}func keyInputButton(_ button: KeyInputButton, didInsertText text: String) {
    textView.insertText(text)
}func keyInputButtonDidDeleteBackward(_ button: KeyInputButton) {
    textView.deleteBackward()
}

点击显示菜单

@objc private func showMenuButtonClicked(_ button: UIButton) {
    button.becomeFirstResponder()    
    NotificationCenter.default.addObserver(self, selector: #selector(menuControllerWillHide), name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)    
    let menu = UIMenuController.shared    let customItem = UIMenuItem(title: "Custom item", action: #selector(customItemDidSelect))
    menu.menuItems = [customItem]
    menu.setTargetRect(button.frame, in: view)    // Display immediately may disappear soon, so display after a little time
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 
        menu.setMenuVisible(true, animated: true)
    }
}    
func customItemDidSelect() {    print(#function)
}@objc private func menuControllerWillHide() {    // Prevent custom menu items from displaying in text view
    UIMenuController.shared.menuItems = nil
    NotificationCenter.default.removeObserver(self, name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
}

由于 button 成为第一响应者时键盘一定会显示,所以每次都可以让 button 调用 becomeFirstResponder 方法。

依然要监听菜单消失,清除自定义菜单按钮,移除通知监听。

需要注意的是,UIMenuController 的 setMenuVisible(_:animated:) 方法要延迟调用,否则菜单可能刚出现就消失。

自定义 Menu controller

由于之前尝试其他方法不满意(当时修改响应链的方法还有问题),于是查找自定义的菜单。找到一个:https://github.com/camelcc/MenuPopOverView
自己也写了一个:https://github.com/Silence-GitHub/SWMenuController
以下介绍自己写的 SWMenuController,先看效果图

1089786-20170508120017957-1639904873.png

1089786-20170508120027519-546002029.png

基本够用,但是和 UIMenuController 还是有差距(例如动画效果、自动调整字体大小等)。

实现原理是,继承 UIView,添加 UIButton 作为菜单按钮,添加到 window 来显示。

与 UIMenuController 相似,但所有菜单按钮都要自定义,传入菜单按钮标题的数组

let menu = SWMenuController()
menu.delegate = selfmenu.menuItems = ["Copy", "Paste", "Select", "Select all", "Look up", "Search", "Delete"]
menu.setTargetRect(frame, in: view)
menu.setMenuVisible(true, animated: true)

实现 SWMenuControllerDelegate 方法,处理第 index 个菜单按钮的点击事件(index 从 0 开始)

func menuController(_ menu: SWMenuController, didSelected index: Int) {    print(menu.menuItems[index])    // Do something for menu at index}


















本文转自xmgdc51CTO博客,原文链接:http://blog.51cto.com/12953214/1940550 ,如需转载请自行联系原作者




版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
iOS 端自定义开发(二)| 学习笔记
快速学习 iOS 端自定义开发。
0 0
iOS 端自定义开发(一)| 学习笔记
快速学习 iOS 端自定义开发。
0 0
iOS 端自定义开发(二)|学习笔记
快速学习iOS 端自定义开发(二)
0 0
iOS 端自定义开发(一)|学习笔记
快速学习iOS 端自定义开发(一)
0 0
iOS开发:简单的Toast提示框实现
博主是以iOS开发出身,那就最后一篇博文就分享一下关于iOS的内容吧。iOS开发过程中,有些时候操作App的时候,需要给用户对应的响应提示操作,使用系统自带的提示框不是每种情况都适用的。
0 0
iOS开发:字符串设置指定内容的文字颜色、文字大小、文字字体类型
在iOS开发过程中,会有一些为了提高APP的视觉效果而设置的特别一点的效果,比如一行文字需要自定义不同的颜色和文字大小,这就用到通过富文本来设置字符串的颜色、大小和文字类型。这篇博文我打算只介绍怎么设置指定内容的一些文字属性设置,如果之前看过我写的博文,就会发现有一篇类似介绍通过富文本来设置字符串内容的博文,但是那篇是综合性的,包括介绍button的,以及UItextfield的设置,所以在这里我只介绍怎么设置字符串指定位置的一些自定义设置的方法,如有不妥之处,欢迎指正。
0 0
iOS开发:平时做项目经常用到的快捷键归纳
从事了这么久编程工作,总结了好多知识,但是本人之前从来没有写博客的习惯,通过去年的一次面试,明白了,不写技术博客会被面试官鄙视,所以本人痛下决心,要改变这个不爱写博客的现状,只要有时间,我就把我从开始iOS编程到现在,总结的所有内容都贴出来,之前是直接截图或者word文档,存在自己电脑里面,现在是时候把它们贴出来了。
0 0
iOS开发:个人对于textView基础用法的总结(其一)
从事了这么久ios开发,对于textView的使用并不陌生,它和textfield有相似的地方,也有不同的地方,这里只对textView的一些基础用法进行描述,textfield不在这里描述。
0 0
iOS开发:报错The sandbox is not in sync with the Podfile.lock. Run 'pod install' …的解决方法
在iOS原生开发的时候,凡是涉及到运行项目之后Xcode提示Build target Runner-Log,是不是每次看到这种报错提示就觉得眼前一黑,头皮发麻?其实也没我说的那么夸张,但是有时候遇到这种问题确实比较费头发。本篇文章就来分享一个常见的问题,但是该问题解决起来不复杂,分享出来,方便后期查阅使用。
0 0
+关注
文章
问答
文章排行榜
最热
最新
相关电子书
更多
React-Native 在iOS上的性能优化方案
立即下载
聚划算iOS平台动态化方案LuaView
立即下载
深入剖析 iOS 性能优化
立即下载