I have a WKWebView
and would like to remove the system menu items (Copy, Define, Share...) from the Edit Menu and present my own.
I am targeting iOS 8 and 9. I am currently testing with the Xcode 7.0.1 simulator (iOS 9) and my iPhone 6 running iOS 9.0.2.
I know the standard way of achieving this is by subclassing WKWebView
and implementing
-canPerformAction:withSender:
. However, I have found that with WKWebView
-canPerformAction:withSender:
is not being called for the copy:
or define:
actions. This appears to be a known bug (WKWebView and UIMenuController).
Example app: https://github.com/dwieringa/WKWebViewCustomEditMenuBug
@implementation MyWKWebView
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
NSLog(@"ACTION: %@", NSStringFromSelector(action));
if (action == @selector(delete:))
{
// adding Delete as test (works)
return YES;
}
// trying to remove everything else (does NOT work for Copy, Define, Share...)
return NO;
}
- (void)delete:(id)sender
{
NSLog(@"Delete menu item selected");
}
@end
Output: (note no copy:
or define:
action)
2015-10-20 12:28:32.864 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: cut:
2015-10-20 12:28:32.865 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: select:
2015-10-20 12:28:32.865 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: selectAll:
2015-10-20 12:28:32.865 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: paste:
2015-10-20 12:28:32.866 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: delete:
2015-10-20 12:28:32.866 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _promptForReplace:
2015-10-20 12:28:32.866 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _transliterateChinese:
2015-10-20 12:28:32.867 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _showTextStyleOptions:
2015-10-20 12:28:32.907 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _addShortcut:
2015-10-20 12:28:32.908 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _accessibilitySpeak:
2015-10-20 12:28:32.908 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _accessibilitySpeakLanguageSelection:
2015-10-20 12:28:32.908 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: _accessibilityPauseSpeaking:
2015-10-20 12:28:32.909 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: makeTextWritingDirectionRightToLeft:
2015-10-20 12:28:32.909 WKWebViewCustomEditMenuBug[45804:21121480] ACTION: makeTextWritingDirectionLeftToRight:
My desire now is to completely hide the edit menu and replace it with a custom menu using QBPopupMenu.
My problem is that I have not been able to find a way to hide or disable the standard Edit menu. I have found some suggestions to hide it with [UIMenuController sharedMenuController].menuVisible = NO;
on UIMenuControllerWillShowMenuNotification
, but I have not been able to get this to work. It has no affect with WillShowMenu
. I can hide it in DidShowMenu
but by that point it is too late and I get a menu flash.
I have also tried to locate it outside the visible area using [[UIMenuController sharedMenuController] setTargetRect:CGRectMake(0, 0, 1, 1) inView:self.extraView];
, but again doing so with WillShowMenu
has no affect and with DidShowMenu
it is too late.
Experiments available here: https://github.com/dwieringa/WKWebViewEditMenuHidingTest
What am I missing? Is there another way to disable or hide the standard editting menu for WKWebView?
Based on your workaround, I found out that:
-(void)menuWillShow:(NSNotification *)notification
{
NSLog(@"MENU WILL SHOW");
dispatch_async(dispatch_get_main_queue(), ^{
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
});
}
Will prevent the menu from flashing 90% of the times.. Still not good enough, but it's another workaround before we find a decent solution.
Try making your view controller become first responder and stop it from resigning first responder
- (BOOL)canResignFirstResponder {
return NO;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
https://github.com/dwieringa/WKWebViewEditMenuHidingTest/pull/1
I Fixed it after some observation.
In -canPerformAction:withSender:
I am returning NO
for _share
and _define
options as I don't need them in my project. It works as expected on selection of word for first time, but shows up the options from second time.
Simple fix: Add [self becomeFirstResponder];
in tapGuesture
or Touch delegate methods
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender {
SEL defineSEL = NSSelectorFromString(@"_define:");
if(action == defineSEL){
return NO;
}
SEL shareSEL = NSSelectorFromString(@"_share:");
if(action == shareSEL){
return NO;
}
return YES;
}
// Tap gesture delegate method
- (void)singleTap:(UITapGestureRecognizer *)sender {
lastTouchPoint = [sender locationInView:self.webView];
[self becomeFirstResponder]; //added this line to fix the issue//
}
Hey guys after spending a hours on it, i found dirty solution with %100 success rate.
Logic is; detect when UIMenuController did shown and update it.
In your ViewController(containing WKWebView) add UIMenuControllerDidShowMenu observer in viewDidLoad() like this;
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(uiMenuViewControllerDidShowMenu),
name: NSNotification.Name.UIMenuControllerDidShowMenu,
object: nil)
}
Don't forget to remove observer in deinit.
deinit {
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name.UIMenuControllerDidShowMenu,
object: nil)
}
And in your selector, update UIMenuController like this:
func uiMenuViewControllerDidShowMenu() {
if longPress {
let menuController = UIMenuController.shared
menuController.setMenuVisible(false, animated: false)
menuController.update() //You can only call this and it will still work as expected but i also call setMenuVisible just to make sure.
}
}
In your ViewController who ever calls the UIMenuController, this method will get called. I am developing browser app so i have also searchBar and user may want to paste text to there. Because of that i detect longPress in my webview and check if UIMenuController is summoned by WKWebView.
This solution will behave like in gif. You can see menu for a second but you can't tap it. You can try to tap it before it fades away but you won't succeed. Please try and tell me your results.
I hope it helps someone.
Cheers.
This bug is actually caused by the actions being added in the WKContentView, which is a private class. You could add a UIView extension to work around it like this:
import UIKit
extension UIView {
open override class func initialize() {
guard NSStringFromClass(self) == "WKContentView" else { return }
swizzleMethod(#selector(canPerformAction), withSelector: #selector(swizzledCanPerformAction))
}
fileprivate class func swizzleMethod(_ selector: Selector, withSelector: Selector) {
let originalSelector = class_getInstanceMethod(self, selector)
let swizzledSelector = class_getInstanceMethod(self, withSelector)
method_exchangeImplementations(originalSelector, swizzledSelector)
}
@objc fileprivate func swizzledCanPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
I tried the solution from Stephan Heilner but it didn't compile in Swift 4.
This is my implementation to disable the menuController in a WKWebView that works with Swift 4.
In my WKWebView subclass, I added these property and function :
var wkContentView: UIView? {
return self.subviewWithClassName("WKContentView")
}
private func swizzleResponderChainAction() {
wkContentView?.swizzlePerformAction()
}
Then, I added an extension in the same file, but out of the WKWebView subclass :
// MARK: - Extension used for the swizzling part linked to wkContentView (see above)
extension UIView {
/// Find a subview corresponding to the className parameter, recursively.
func subviewWithClassName(_ className: String) -> UIView? {
if NSStringFromClass(type(of: self)) == className {
return self
} else {
for subview in subviews {
return subview.subviewWithClassName(className)
}
}
return nil
}
func swizzlePerformAction() {
swizzleMethod(#selector(canPerformAction), withSelector: #selector(swizzledCanPerformAction))
}
private func swizzleMethod(_ currentSelector: Selector, withSelector newSelector: Selector) {
if let currentMethod = self.instanceMethod(for: currentSelector),
let newMethod = self.instanceMethod(for:newSelector) {
let newImplementation = method_getImplementation(newMethod)
method_setImplementation(currentMethod, newImplementation)
} else {
print("Could not find originalSelector")
}
}
private func instanceMethod(for selector: Selector) -> Method? {
let classType = type(of: self)
return class_getInstanceMethod(classType, selector)
}
@objc private func swizzledCanPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
And finally, I called the swizzleResponderChainAction()
function from the initializer (you can either override the designated initializer, or create a convenience one):
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
swizzleResponderChainAction()
}
Now, the WKWebView does not crash anymore when using a UIMenuController.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With