iOS uses UIMenuController without hiding keyboard

iOS uses UIMenuController without hiding keyboard

Use UIMenuController pop-up menu when keyboard display, keep keyboard display and input state.

The implementation methods are as follows:

  1. Modify the response chain (recommendation)
  2. Follow the UIKeyInput protocol
  3. Customize Menu controller

The code for the first two methods has been uploaded to GitHub: https://github.com/Silence-GitHub/MenuControllerDemo
The GitHub link for the third method: https://github.com/Silence-GitHub/SWMenuController

Before that, I introduced the use of UIMenuController and the reasons why keyboards are hidden.

If you only need to implement the function, you can look at the code of the first method, and you don't need to look at the text. If you want to understand the principles of Responder chain, look at Apple's documentation first. Understanding Responders and the Responder Chain

Usage of UIMenuController

Customize a view that needs to display UIMenuController. Take UIButton as an example, and customize the class 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 must overload the canBecomeFirstResponder property and return true to display the menu (UIMenuController). The first responder can process the menu. If the canBecomeFirstResponder returns false and cannot be the first responder, the menu will not be displayed.

Overload the canPerformAction (: withSender:) method and filter the UIMenu Item that needs to be displayed. Parametric actions have methods for UIResponder Standard EditAction protocols such as copy(:), paste(:). Return true for the required operation and display the menu button (the code above shows the "Copy" menu button); return false for the unnecessary operation and try to hide the menu button (the menu button is not necessarily hidden, if other responders in the response chain return true, the menu button will still be displayed). By default (when this method is not implemented), if the current class implements the corresponding action, it returns true; if the corresponding action is not implemented, it calls the method of the next responder. If this method is not implemented (or this method returns false), the responder on the response chain does not implement this method (or this method returns true) but implements the copy(:) method, the "Copy" menu button will be displayed. It is recommended that this method be implemented, at least at this level of the response chain, to control the menu buttons.

Implement the action method corresponding to the menu button that needs to be displayed. The above code is the copy(:) method. When the menu button is clicked, the action method is sent. If the canPerformAction (: withSender:) method is not implemented, UIKit will follow the response chain to find the responder who implements the action and send the action method to the responder who implements the action. Once the canPerformAction (: withSender:) method is implemented and true is returned, the action method will be sent to the current responder, and the responder that implements the action will not be found along the response chain, so the corresponding action method must be implemented.

In the UIViewController, let the custom Show MenuButton listen for click events

button.addTarget(self, action: #selector(showMenuButtonClicked(_:)), for: .touchUpInside)

Click on the button pop-up menu

@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)
}

Before using UIMenuController, the menu can be displayed by making the button the first responder.

The controller does not implement the canPerformAction (: withSender:) method, and implements customItemDidSelect. The current controller can be found along the response chain from the button, so the custom menu button can be displayed. If the controller implements the canPerformAction (: withSender:) method and returns false, the custom menu button will not be displayed.

Hide menus if necessary

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

Note that UIMenuController has only one instance. Hidden menuItems retain the value when they are displayed. The next time they are displayed elsewhere, old custom menu buttons will appear, so update the menuItems property at the appropriate time.

UITextView and UITextField become the first responders (click on the input box to prepare for input), and the keyboard will display. The input box is not the first responder, and the keyboard is hidden. Because the custom control to display the menu calls the becomeFirstResponder() method and becomes the first responder, the input box is not the first responder, so the keyboard is hidden.

The Method of Not Hiding Keyboard

Modify the response chain (recommendation)

This is the best method at present, with the least amount of code. UIMenuController can be used normally, and the keyboard can display and input normally, the cursor of the input box still flickers.

Methodological ideas come from: http://stackoverflow.com/questions/13601643/uimenucontroller-hides-the-keyboard
However, those codes still have bug s, which will be solved here. Since the input box loses the first responder and the keyboard hides, let the input box remain the first responder. By changing the response chain, the menu events are passed to the responders who can handle them.

Take UITextView as an example, custom class 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)
    }
}

Overload the next attribute and change the response chain. The overloaded canPerformAction (: withSender:) method returns false when the response chain changes.

The controller code needs to be modified

// 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)
}

If text view is not the first responder, the keyboard is not displayed, as it was. If text view is the first responder, change the response chain so that the next responder of the input box becomes a button. The menu will display which buttons, starting with the first responder text view, along the response chain, through the canPerform Action (: withSender:) method to determine. Although the text view's canPerformAction (: withSender:) method returns false, the button's canPerformAction (: withSender:) method returns true to the copy(:) method, so the "Copy" menu button is displayed. Click on the "Copy" menu button and the button will execute the copy(:) method. The controller also implements the customItemDidSelect method on this response chain. If the canPerformAction (: withSender:) method is not implemented, the canPerformAction (: withSender:) method returns true to the customItemDidSelect method by default, so the custom menu button will be displayed. Click the custom menu button, and the controller executes the customItemDidSelect method.

The listener menu disappears. When it is about to disappear, the response chain is restored, the custom menu button is cleared, and the notification listener is removed.

The input box can also display the menu itself. If you click on the button first, then click on the text view, and let the text view display the menu, the custom menu button will still be displayed. Since the listener menu has not disappeared, the custom menu button has not been cleared. So listen for keyboard display

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

Clear the custom menu button when the keyboard will be displayed and remove the notification listener before the controller is released

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

deinit {
    NotificationCenter.default.removeObserver(self)
}

Follow the UIKeyInput protocol

This method must show the keyboard, not hide the keyboard. Meanwhile, the cursor of the input box does not flicker. Generally speaking, it can input normally, but the Chinese input method only responds to some keys (return, blank, etc.).

Methodological ideas come from: http://stackoverflow.com/questions/4282964/becomefirstresponder-without-hiding-keyboard/4284675#4284675
There are also code examples of this method on GitHub: https://github.com/jaredsinclair/UIMenuControllerTest
Although bug s in those codes will be fixed here, problems such as non-flickering cursors in input boxes remain. The UIResponder following the UIKeyInput protocol becomes the first responder, and the keyboard pops up.

Take UIButton as an example, custom class 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)
    }
}

The method of UIKeyInput protocol is related to keyboard input. The hasText method indicates whether there is text or not. The deleteBackward method is called when the delete key of the keyboard is clicked. The insertText(:) method is called when the keyboard is entered. Make the controller a delegate for the button and pass these methods to the text view (UITextView, without customization)

func keyInputButtonHasText(_ button: KeyInputButton) -> Bool {
    return textView.hasText
}

func keyInputButton(_ button: KeyInputButton, didInsertText text: String) {
    textView.insertText(text)
}

func keyInputButtonDidDeleteBackward(_ button: KeyInputButton) {
    textView.deleteBackward()
}

Click on the Display Menu

@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)
}

Because the keyboard will be displayed when the button becomes the first responder, you can have the button call the becomeFirstResponder method each time.

The listener menu still disappears, the custom menu button is cleared, and the notification listener is removed.

It should be noted that UIMenuController's setMenuVisible (: animated:) method needs to be delayed, otherwise the menu may disappear as soon as it appears.

Customize Menu controller

Since the previous attempt was unsatisfactory (there was still a problem with modifying the response chain), a custom menu was found. Find one: https://github.com/camelcc/MenuPopOverView
I wrote one myself: https://github.com/Silence-GitHub/SWMenuController
Here's a description of the SWMenu Controller I wrote. Let's look at the results first.

Basic enough, but there is still a gap with UIMenuController (such as animation effects, automatic font size adjustment, etc.).

The principle is to inherit UIView, add UIButton as a menu button, and add it to the window to display.

Similar to UIMenuController, but all menu buttons need to be customized, passing in an array of menu button titles

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)

Implement the SWMenuController Delegate method to handle the click event of the index menu button (index starts from 0)

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

For reprinting, please indicate the source: http://www.cnblogs.com/silence-cnblogs/p/6824426.html

Keywords: iOS github Attribute emoji

Added by matthiasone on Wed, 03 Jul 2019 00:17:44 +0300