I am trying to override the default UIMenuController so that only my custom item "Define..." appears when the user selects text in its text view. I haven't had much luck with the approaches I've found online thus far.
To be more specific, I have subclassed a UIViewController and used canPerformAction() to exclude all actions except my define method.
override func becomeFirstResponder() -> Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
let canPerform: Bool
if action == #selector(defineWord){
canPerform = true
}
else {
canPerform = false
}
print("action = \(action), canPerform = \(canPerform)")
return canPerform
}
In the view controller's viewDidLoad(), I've included the following:
let shared = UIMenuController.shared
let menuItemDefine = UIMenuItem(title: "Define...", action: #selector(self.defineWord))
shared.menuItems = [menuItemDefine]
Whenever I select text in the view, the console goes through each possible action that might appear in the UIMenuController and says they can't be performed, with the exception of my custom action:
action = cut:, canPerform = false
action = select:, canPerform = false
(and so on, until...)
action = defineWord, canPerform = true
But the resulting edit menu contains "Copy", "Look Up", "Share", and "Define...". These don't appear in the console, which makes me think that a different approach is called for.
Note that I've also tried subclassing UITextView and using the above code as appropriate, but the result is the same.
Any ideas where I'm going wrong?
This might help everyone who is asking this question that how to remove "Copy", "Select All" etc.. standard menu items or UIResponderStandardEditActions that are still visible when you have already returned false in canPerformAction:.
It is related to responder chain. As canPerformAction: is called for every responder, for some of those it may be returning true in canPerformAction: as a default value.
Thus to check where it is failing I found it by overriding this canPerformAction: for every element I used in my controller
For example in my view controller I had a webview and the mistake I was doing was that I was overriding the canPerformAction: in the delegate methods i.e I was doing something like below
extension viewcontroller: UIWebViewDelegate{
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
But the point is that you have to do it for element and not as the delegate method.
extension UIView {
func dropRoundCorners() {
self.layer.cornerRadius = 10.0;
self.clipsToBounds = true;
}
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
extension UIImageView{
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
extension UIScrollView{
open override func canPerformAction(_ action: Selector, withSender
sender: Any?) -> Bool {
return false
}
}
extension UISlider{
open override func canPerformAction(_ action: Selector, withSender
sender: Any?) -> Bool {
return false
}
}
extension UIWebView{
open override func canPerformAction(_ action: Selector, withSender
sender: Any?) -> Bool {
return false
}
}
I hope this is useful to anyone whose is stuck with this issue.
Following are links that might help you with details:
UIResponder reference
very important read the discussion here regarding responder
some what related
May be it is too late for the answer, but it can be helpful for other users.
So, my solution is: I created custom UITextView and redefined the following methods:
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
//Here you can check for all action what you need
return (action == #selector(yourCustomAction)) ? YES : NO;
}
Related
I am currently trying to prevent all UIResponderStandardEditActions like copy, paste, delete from showing up when the UITextfield is empty. I would only like to show them if the user has types a message. I have tried 2 solutions and currently don't work, I'm not sure if its to do with iOS 12 or. I have tried overriding the canPerformAction method both in a UITextfield extension and using a custom class later assigned to the UITextfield in the Storyboard but no luck. Is there another way to do this. Here is what I have tried.
extension UITextField {
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if self.text!.isEmpty {
return false
}
return action == #selector(UIResponderStandardEditActions.paste(_:))
}
}
class CustomTextField: UITextField {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(UIResponderStandardEditActions.paste(_:)) || action == #selector(UIResponderStandardEditActions.copy(_:)) || action == #selector(UIResponderStandardEditActions.delete(_:)) {
return false
}
return true
}
}
Just override with your subclass and use this class instead of UITextField. By the way, this will disable copy paste,cut for whole conditions so that you need to add some hasText: Bool or related condition to the switch case.
#IBDesignable
class ActionsDisabledUITextField: UITextField {
#IBInspectable var isPasteEnabled: Bool = false
#IBInspectable var isSelectEnabled: Bool = false
#IBInspectable var isSelectAllEnabled: Bool = false
#IBInspectable var isCopyEnabled: Bool = false
#IBInspectable var isCutEnabled: Bool = false
#IBInspectable var isDeleteEnabled: Bool = false
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
switch action {
case #selector(UIResponderStandardEditActions.paste(_:)) where !isPasteEnabled,
#selector(UIResponderStandardEditActions.select(_:)) where !isSelectEnabled,
#selector(UIResponderStandardEditActions.selectAll(_:)) where !isSelectAllEnabled,
#selector(UIResponderStandardEditActions.copy(_:)) where !isCopyEnabled,
#selector(UIResponderStandardEditActions.cut(_:)) where !isCutEnabled,
#selector(UIResponderStandardEditActions.delete(_:)) where !isDeleteEnabled:
return false
default:
//return true : this is not correct
return super.canPerformAction(action, withSender: sender)
}
}
}
Displaying a PDFDocument in a PDFView allows the user to select parts of the document and perform actions e.g. "copy" with the selection.
How can selection be disabled in a PDFView while preserving the possibility for the user to zoom in and out and scroll in the PDF?
PDFView itself does not seem to offer such a property nor does the PDFViewDelegate.
You have to subclass PDFView, as such:
class MyPDFView: PDFView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer is UILongPressGestureRecognizer {
gestureRecognizer.isEnabled = false
}
super.addGestureRecognizer(gestureRecognizer)
}
}
Just need to do is it will auto clear the selection and User will no longer long-press on PDF text.
class MyPDFView: PDFView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
self.currentSelection = nil
self.clearSelection()
return false
}
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer is UILongPressGestureRecognizer {
gestureRecognizer.isEnabled = false
}
super.addGestureRecognizer(gestureRecognizer)
}
}
This below 2 lines need to add in canPerformAction()
self.currentSelection = nil
self.clearSelection()
For iOS 13, the above solution no longer works. It looks like they've changed the internal implementation of PDFView and specifically how the gesture recognizers are set up. I think generally it's discouraged to do this kind of thing, but it can still be done without using any internal API, here's how:
1) Recursively gather all subviews of PDFView (see below for the helper function to do this)
let allSubviews = pdfView.allSubViewsOf(type: UIView.self)
2) Iterate over them and deactivate any UILongPressGestureRecognizers:
for gestureRec in allSubviews.compactMap({ $0.gestureRecognizers }).flatMap({ $0 }) {
if gestureRec is UILongPressGestureRecognizer {
gestureRec.isEnabled = false
}
}
Helper func to recursively get all subviews of a given type:
func allSubViewsOf<T: UIView>(type: T.Type) -> [T] {
var all: [T] = []
func getSubview(view: UIView) {
if let aView = view as? T {
all.append(aView)
}
guard view.subviews.count > 0 else { return }
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
I'm calling the above code from the viewDidLoad method of the containing view controller.
I haven't yet found a good way to work this into a subclass of PDFView, which would be the preferred way for reusability and could just be an addition to the above NonSelectablePDFView. What I've tried so far is overriding didAddSubview and adding the above code after the call to super, but that didn't work as expected. It seems like the gesture recognizers are only being added at a later step, so figuring out when that is and if there's a way for the subclass to call some custom code after this happened would be a next step here.
With Swift 5 and iOS 12.3, you can solve your problem by overriding addGestureRecognizer(_:) method and canPerformAction(_:withSender:) method in a PDFView subclass.
import UIKit
import PDFKit
class NonSelectablePDFView: PDFView {
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
(gestureRecognizer as? UILongPressGestureRecognizer)?.isEnabled = false
super.addGestureRecognizer(gestureRecognizer)
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
As an alternative to the previous implementation, you can simply toggle UILongPressGestureRecognizer isEnabled property to false in the initializer.
import UIKit
import PDFKit
class NonSelectablePDFView: PDFView {
override init(frame: CGRect) {
super.init(frame: frame)
if let gestureRecognizers = gestureRecognizers {
for gestureRecognizer in gestureRecognizers where gestureRecognizer is UILongPressGestureRecognizer {
gestureRecognizer.isEnabled = false
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
You should note that this is not sufficient to disable text selecting, as there is a UITapAndHalfRecognizer as well – obviously a private Apple class - that also creates selections.
It is attached to the PDFDocumentView, which is another private implementation detail of PDFView, and which you can not replace with your own class implementation.
I show a UIMenuController in a uiviewconroller in this way:
in my class:
override open func canBecomeFirstResponder() -> Bool {
return true
}
open override func canPerformAction(_ action: Selector, withSender sender: Any) -> Bool {
//here I check for my custom action, else return false
return false
}
then to show I use:
//Make this as first responder
self.becomeFirstResponder()
///Build menu
let menu = UIMenuController.shared
///Set item and anchor point, and showit
menu.menuItems = itemsToAdd
menu.setTargetRect(CGRect(x: 0, y: 5, width: bubbleNode.view.bounds.size.width, height: bubbleNode.view.bounds.size.height), in: bubbleImageNode.view)
menu.setMenuVisible(true, animated: true)
the problem is that in a device I show my custom items, but also: "Spell, speak, speck sentence, ecc..." how can I disable it?
override canPerformAction and handle it for each specific action. It working perfectly for me.
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
var canPerform = super.canPerformAction(action, withSender: sender)
if (action == "your action to restrict") {
canPerform = false
}
return canPerform
}
Ok, the problem is "Accessibility" option "speak selection" in my device, if I disabled it, I see only the custom items, but in other app i see only custom items with this option enabled!
class TextViewWithCopyAction: UITextView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(copy(_:)){
return true
} else {
return false
}
}
}
to use it:
let textView: TextViewWithCopyAction = {
let textView = TextViewWithCopyAction()
textView.backgroundColor = .white
return textView
}()
If you see speak option in UIMenuController that is from device go to:
Setting->Accessibility->Speech->Speech
Select and disable it. Automatically Speak option will get removed from menu.
Can Anyone help me, i'm having problem with UIMenucontroller.In here, i have to use two menucontroller in single viewcontroller.
For First menu only "paste",for other menu "copy","select","select all" When i'm using shared menucontroller it affects the other menu.
My code for first menu is as follows:
override func canBecomeFirstResponder() -> Bool
{
return true
}
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool
{
//actions
}
UIMenuController.sharedMenuController().menuItems = nil
let Select: UIMenuItem = UIMenuItem(title: "Select", action: Selector("Select"))
let SelectAll: UIMenuItem = UIMenuItem(title: "SelectAll", action: Selector("SelectAll"))
let Copy: UIMenuItem = UIMenuItem(title: "Copy", action: Selector("Copy"))
let menu: UIMenuController = UIMenuController.sharedMenuController()
menu.menuItems = [Select,SelectAll,Copy]
menu.setTargetRect(cell.frame, inView: cell.superview!)
menu.setMenuVisible(true, animated: true)
and my second menu is:
UIMenuController.sharedMenuController().menuVisible = false
let paste: UIMenuItem = UIMenuItem(title: "Paste", action: Selector("paste"))
let menu: UIMenuController = UIMenuController.sharedMenuController()
menu.menuItems = [paste]
menu.setTargetRect(message_Textfield.frame, inView: message_Textfield.superview!)
menu.setMenuVisible(true, animated: true)
Error:
In here,in second menu contains unwanted things as [Select,SelectAll,Copy] with [Paste].
How can i resolve this,thanks in advance
You should override canPerformAction in a UITextField subclass to disable the item you don't want, then assign each uitextfield you created to the subclass.
For example, disable paste menu item in uimenucontroller:
class CustomTextField: UITextField {
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == "paste:" {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
Usage:
let message_Textfield = CustomTextField()
Now paste menu item will be disabled for message_Textfield
Hi Thanks for your answer,But i found solution as follows:
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool
{
if(MenuBool == true){
if action == Selector("Copy") || action == Selector("star") || action == Selector("info") || action == Selector("forward") || action == Selector("Delete")
{
print("UIMenuController====>CellMenu")
UIMenuController.sharedMenuController().menuVisible = false
return true
}
print("UIMenuController====>Defaultmenu1")
return false
}else if MenuBool == false
{
print("UIMenuController====>Defaultmenu2")
return false
}else{
print("UIMenuController====>DefaultmenuElse")
return false
}
}
In,this manner working fine.
:):):)
I managed to subclass UITextView and disabled the "Define" contextual menu item.
class TextViewer: UITextView {
// Overide, disable the "Define" contextual menu item
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == "_define:" {
return false;
}
return super.canPerformAction(action, withSender: sender)
}
}
But now, how do I add a different/custom menu item to take its place? I was guessing it might be something like this, but it's not quite right.
var menuController = UIMenuController.sharedMenuController()
var customMenuItem = UIMenuItem(title: "Lookup", action: "lookupWord")
menuController.menuItems?.append(customMenuItem)
Thanks for any help you folks can give me. ;-)
Nevermind:
let mnuController = UIMenuController.sharedMenuController()
let lookupMenu = UIMenuItem(title: "Tshawb", action: "tshawb")
mnuController.menuItems = NSArray(array: [lookupMenu])
// This makes the menu item visible.
mnuController.setMenuVisible(true, animated: true)