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.
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.
To add/remove a UIMenuItem to/from an Array, we do:
1 2 3 4 |
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:
1 2 3 4 |
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:
1 2 3 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
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:
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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 thetextFieldDidBeginEditing
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, theendEditing
call triggers the textFieldDidBeginEditing as well inside which we remove the responder from theUITextField
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:
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