Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable entire UIMenuController edit menu in WKWebView

Requirement

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.

Standard Method Does Not Work

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:

Planned Workaround

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?

like image 470
davew Avatar asked Oct 20 '15 17:10

davew


6 Answers

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.

like image 148
Paulo Cesar Avatar answered Nov 03 '22 19:11

Paulo Cesar


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

like image 27
ranunez Avatar answered Nov 03 '22 19:11

ranunez


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//
}
like image 28
Lax Avatar answered Nov 03 '22 21:11

Lax


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.

enter image description here

like image 44
ysnzlcn Avatar answered Nov 03 '22 21:11

ysnzlcn


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
    }
}
like image 2
Stephan Heilner Avatar answered Nov 03 '22 21:11

Stephan Heilner


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.

like image 2
Frédéric Adda Avatar answered Nov 03 '22 21:11

Frédéric Adda