iOS UIMenuController UIMenuItem Part 2

Filed Under: iOS

This is the second part in the series of tutorials on UIMenuController. In this tutorial, we’ll be implementing the UIMenuController on a Label and UITextField. We’ll see how can switch from one to the other. Also, we’ll be implementing Custom Copy Paste using UIPasteboard on a UIMenuItem in our iOS Application.

UIMenuController and UIMenuItem

In the previous tutorial, we saw how to add a UIMenuController on a UITextField.
Now a UITextField by default has events to detect touch events and is the first responder as well.

In order to show a UIMenuController on a UILabel, we need to do the following things:

  • Override canBecomeFirstResponder property and return true.
  • Execute becomeFirstResponder() method on the UILabel.
How do we set touch events on a UILabel?

Answer: UIGestureRecogniser

Now, using a UIGestureRecogniser we can set a long press on the UILabel. Doing so, we will show the UIMenuController over the UILabel.

How is it positioned over the label?

By passing the UILabel reference in the setTargetRect method as we had discussed in the previous tutorial.

How will we set the UIResponder back to the UITextField when it is clicked again?

In order to do this, we must override the textFieldDidBeginEditing and textFieldDidEndEditing.
The first would get triggered when the UITextField is clicked. Here we will set the becomeFirstResponder() on the UITextField instance.

In the textFieldDidEndEditing, we will call resignFirstResponder.

We must create a subclass of UILabel inside which we will override the canPerformAction method. Here we will enable the UIMenuItems for the UIlabel only.

Now since we need to use UILabel and UITextField together with the UIMenuController, we should subclass the UITextField as well and implement the canPerformAction of it separately. This way the canPerformAction of each of the subclasses would be independent of one another.

Important Methods of UIMenuController

To add/remove a UIMenuItem to/from an Array, we do:


menuController?.menuItems?.append(newElement: UIMenuItem).
menuController?.menuItems?.remove(at: Int)

The first method appends a UIMenuItem to the end of the array and second removes the UIMenuItem from an index passed.

We have two more variants for adding and removing UIMenuItems:


menuController?.menuItems?.insert(newElement:UIMenuItem, at:  Int)
menuController?.menuItems?.removeAll(where: (UIMenuItem) throws -> Bool)

The second method, removes all UIMenuItems which matches the Predicate.
Example to remove all Menu Items with the title “Menu JD” we do:


menuController?.menuItems?.removeAll(where: {$0.title == "Menu JD"})

Updating the Menu
Once you modify a UIMenuController and change any of the UIMenuItem, you must call update() on the UIMenuController for updating the Menu with the new appearance. Otherwise, the UIMenuController, would still show the previous Menu.

Let’s talk code in the next section with our sample iOS Application.

Code

Let’s write the subclass for UITextField first:

JDTextField.swift


import UIKit

class JDTextField : UITextField {
    
    
    override var canBecomeFirstResponder: Bool {
        get {
            return true
        }
    }
    
    @objc public func onMenu1(sender: UIMenuItem) {
        print("onMenu1 textfield")
    }
    
    @objc public func onMenu2(sender: UIMenuItem) {
        print("onMenu2 textfield")
    }
    
    @objc public func onMenu3(sender: UIMenuItem) {
        print("onMenu3 textfield")
        let menuItem4: UIMenuItem = UIMenuItem(title: "Menu 4", action: #selector(onMenu4(sender:)))
        let myIndex = menuController?.menuItems?.firstIndex(where: {$0.title == "Menu 3"})
        menuController?.menuItems?.remove(at: myIndex!)
        menuController?.menuItems?.insert(menuItem4, at:  myIndex!)
        menuController?.update()
        
    }
    
    @objc public func onMenu4(sender: UIMenuItem) {
        let menuItem3: UIMenuItem = UIMenuItem(title: "Menu 3", action: #selector(onMenu3(sender:)))
        let myIndex = menuController?.menuItems?.firstIndex(where: {$0.title == "Menu 4"})
        menuController?.menuItems?.removeAll(where: {$0.title == "Menu 4"})
        menuController?.menuItems?.insert(menuItem3, at:  myIndex!)
        menuController?.update()
        print("onMenu4 textfield")
    }
    
    
    @objc public func customPaste(sender: UIMenuItem) {
        self.text = UIPasteboard.general.string
        menuController?.setMenuVisible(false, animated: true)
        
    }
    
    override func canPerformAction(_ action: Selector, withSender sender: Any!) -> Bool {
        if [#selector(onMenu1(sender:)), #selector(onMenu2(sender:)), #selector(onMenu3(sender:)),#selector(onMenu4(sender:)),#selector(customPaste(sender:))].contains(action) {
            return true
        } else {
            return false
        }
    }
}

In the above code, we have the implementation of the 4 UIMenuItem actions.
When Menu 3 is clicked, we remove it and add Menu 4 and vice-versa.
Inside the customPaste action, we are pasting the string from the UIPasteBoard.



Let’s next look at the implementation of JDLabel.swift



import UIKit


class JDLabel : UILabel {
    
    override var canBecomeFirstResponder: Bool {
        get {
            return true
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        print("required init")
        sharedInit()
        showLabelMenu()
    }
    
    func showLabelMenu()
    {
        menuController = UIMenuController.shared
        menuController?.isMenuVisible = true
        menuController?.arrowDirection = UIMenuController.ArrowDirection.down
        
        menuController?.setTargetRect(CGRect.zero, in: self)
        
        let menuItem_1: UIMenuItem = UIMenuItem(title: "Menu 1", action: #selector(JDLabel.onMenu1(sender:)))
        let menuItem_2: UIMenuItem = UIMenuItem(title: "Menu 2", action: #selector(JDLabel.onMenu2(sender:)))
        let menuItem_3: UIMenuItem = UIMenuItem(title: "Menu 3", action: #selector(JDLabel.onMenu3(sender:)))
        
        let myMenuItems: [UIMenuItem] = [menuItem_1, menuItem_2, menuItem_3]
        menuController?.menuItems = myMenuItems
    }
    
    func sharedInit() {
        isUserInteractionEnabled = true
        addGestureRecognizer(UILongPressGestureRecognizer(
            target: self,
            action: #selector(firstResponder(sender:))
        ))
    }
    
    @objc func firstResponder(sender: Any?) {
        becomeFirstResponder()
        menuController?.setTargetRect(bounds, in: self)
        menuController?.setMenuVisible(true, animated: true)
        menuController?.update()
    }
    
    override func copy(_ sender: Any?) {
        UIPasteboard.general.string = text
        UIMenuController.shared.setMenuVisible(false, animated: true)
    }
    
    @objc public func onMenu1(sender: UIMenuItem) {
        print("onMenu1 label")
    }
    
    @objc public func onMenu2(sender: UIMenuItem) {
        print("onMenu2 label")
    }
    
    @objc public func onMenu3(sender: UIMenuItem) {
        print("onMenu3 label")
        
        let menuItem4: UIMenuItem = UIMenuItem(title: "Menu 4", action: #selector(onMenu4(sender:)))
        let myIndex = menuController?.menuItems?.firstIndex(where: {$0.title == "Menu 3"})
        menuController?.menuItems?.remove(at: myIndex!)
        menuController?.menuItems?.insert(menuItem4, at:  myIndex!)
        
        menuController?.update()
        
    }
    
    @objc public func onMenu4(sender: UIMenuItem) {
        let menuItem3: UIMenuItem = UIMenuItem(title: "Menu 3", action: #selector(onMenu3(sender:)))
        let myIndex = menuController?.menuItems?.firstIndex(where: {$0.title == "Menu 4"})
        menuController?.menuItems?.remove(at: myIndex!)
        menuController?.menuItems?.insert(menuItem3, at:  myIndex!)
        menuController?.update()
        print("onMenu4 label")
    }
    
    
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        if action == #selector(copy(_:)){
            return true
        }
        
        if [#selector(onMenu1(sender:)), #selector(onMenu2(sender:)),#selector(onMenu3(sender:)),#selector(onMenu4(sender:))].contains(action) {
            return true
        } else {
            return false
        }
        
    }
    
}

In the above code, sharedInit() is where we set isUserInteractionEnabled to true and add the gesture recognizer.
showLabelMenu() is where we initialise the UIMenuController
When the long press Gestture happens, we call the function firstResponder. Here we set the UILabel to becomeFirstResponder in order to display the UIMenuController when the Label is clicked.
Besides the three actions, we have implemented the copy action as well in the Menu.
Inside the copy function, we use UIPasteBoard class to copy the label text. This is eventually pasted in the UITextField.

Our Main.storyboard looks like this:

ios uimenucontroller uipasteboard storyboard

Make sure that you’ve set the subclass in the Attributes Inspector for each of them.

The code for the ViewController.swift is given below:


import UIKit

var menuController: UIMenuController?

class ViewController: UIViewController, UITextFieldDelegate {

    
    @IBOutlet weak var myTextField: JDTextField!
    @IBOutlet weak var myLabel: JDLabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myTextField.delegate = self
        
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }

    
    func textFieldDidEndEditing(_ textField: UITextField) {
        textField.resignFirstResponder()
    }
    
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        textField.becomeFirstResponder()
        menuControllerForTextField()
    }


    
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        self.view.endEditing(true)
    }
    
    func menuControllerForTextField()
    {
        menuController = UIMenuController.shared
        menuController?.isMenuVisible = true
        menuController?.arrowDirection = UIMenuController.ArrowDirection.down
        menuController?.setTargetRect(CGRect.zero, in: self.view)
        
        let menuItem1: UIMenuItem = UIMenuItem(title: "Menu 1", action: #selector(JDTextField.onMenu1(sender:)))
        let menuItem2: UIMenuItem = UIMenuItem(title: "Menu 2", action: #selector(JDTextField.onMenu2(sender:)))
        let menuItem3: UIMenuItem = UIMenuItem(title: "Menu 3", action: #selector(JDTextField.onMenu3(sender:)))
        let menuItemPaste: UIMenuItem = UIMenuItem(title: "Paste", action: #selector(JDTextField.customPaste(sender:)))
        
        let myMenuItems: [UIMenuItem] = [menuItem1, menuItem2, menuItem3, menuItemPaste]
        menuController?.menuItems = myMenuItems
        menuController?.update()
    }

}

A lot is happening above:

  • menuControllerForTextField() is called when the textFieldDidBeginEditing gets triggered(on focusing UITextField).
  • Inside menuControllerForTextField() we create the UIMenuController for the UITextField with 4 UIMenuItems.
  • Actions for each of them were defined in the JDTextField.swift class.
  • Note: we’ve declared UIMenuController instance outside the class to use it globally.
  • touchesBegan gets triggered when the user clicks anywhere on the View outside of the UITextField. In this case, the endEditing call triggers the textFieldDidBeginEditing as well inside which we remove the responder from the UITextField
  • textFieldShouldReturn is called when the return key is pressed on the keyboard. Here also we resign the responder in order to dismiss the keyboard.

The output of the application in action is given below:

ios-uimenucontroller uipasteboard final output

So in the above output, we were able to implement copy paste as well as adding and removing menu items in our UIMenuController. When you click on any of the Menu Items, you’ll notice that the methods defined in their respective subclass are executed.

This brings an end to this tutorial. We used UIPasteboard along with UIMenuController. You can download the project from the link below

Comments

  1. amar says:

    Hi ,
    I have an requirement to add new menu items to context menu along with copy,paste,lookup etc.

    I need to do it for system level i.e. when I highlight the text in any app in ios mobile I need to get the menu which I add.

    i am able to do it within app but it needs to be added for all apps.

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages