iOS 使用 UIMenuController 且不隱藏鍵盤的方法 在鍵盤顯示的時候使用 UIMenuController 彈出菜單,保持鍵盤顯示且可輸入的狀態。 實現方法有 1. 修改響應鏈(推薦) 2. 遵循 UIKeyInput 協議 3. 自定義 Menu controller 前兩種方法的 ...
iOS 使用 UIMenuController 且不隱藏鍵盤的方法
在鍵盤顯示的時候使用 UIMenuController 彈出菜單,保持鍵盤顯示且可輸入的狀態。
實現方法有
- 修改響應鏈(推薦)
- 遵循 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 的使用方法
自定義一個需要顯示 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 action
func 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 load
var 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,先看效果圖
基本夠用,但是和 UIMenuController 還是有差距(例如動畫效果、自動調整字體大小等)。
實現原理是,繼承 UIView,添加 UIButton 作為菜單按鈕,添加到 window 來顯示。
與 UIMenuController 相似,但所有菜單按鈕都要自定義,傳入菜單按鈕標題的數組
let menu = SWMenuController()
menu.delegate = self
menu.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
}
轉載請註明出處:http://www.cnblogs.com/silence-cnblogs/p/6824426.html