在WKWebView中禁用整个UIMenuController编辑菜单

时间:2015-10-20 17:37:43

标签: ios wkwebview uimenucontroller

要求

我有一个WKWebView,想要从编辑菜单中删除系统菜单项(复制,定义,共享...)并显示我自己的项目。

我的目标是iOS 8和9.我目前正在使用Xcode 7.0.1模拟器(iOS 9)和运行iOS 9.0.2的iPhone 6进行测试。

标准方法不起作用

我知道实现这一目标的标准方法是通过继承WKWebView并实施 -canPerformAction:withSender:。但是,我发现WKWebView -canPerformAction:withSender:并未调用copy:define:行为。这似乎是一个已知错误(WKWebView and UIMenuController)。

示例应用: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

输出:(注意没有copy:define:操作)

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:

计划的解决方法

我现在的愿望是完全隐藏编辑菜单,并使用QBPopupMenu将其替换为自定义菜单。

我的问题是我无法找到隐藏或禁用标准“编辑”菜单的方法。我在[UIMenuController sharedMenuController].menuVisible = NO;上找到了使用UIMenuControllerWillShowMenuNotification隐藏它的一些建议,但我无法让它工作。它对WillShowMenu没有影响。我可以在DidShowMenu隐藏它,但到那时为时已晚,我得到一个菜单闪烁。

我还尝试使用[[UIMenuController sharedMenuController] setTargetRect:CGRectMake(0, 0, 1, 1) inView:self.extraView];在可见区域之外找到它,但是再次使用WillShowMenu这样做没有任何影响,而使用DidShowMenu则为时已晚。

此处提供的实验:https://github.com/dwieringa/WKWebViewEditMenuHidingTest

我错过了什么?是否有其他方法可以禁用或隐藏WKWebView的标准编辑菜单?

12 个答案:

答案 0 :(得分:6)

尝试让您的视图控制器成为第一响应者并阻止其退出第一响应者

- (BOOL)canResignFirstResponder {
    return NO;
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

https://github.com/dwieringa/WKWebViewEditMenuHidingTest/pull/1

答案 1 :(得分:5)

根据您的解决方法,我发现:

rem Saved in D:\Temp\WriteText.bat
@echo off
setlocal enabledelayedexpansion
ren test.txt in.tmp
set /p line1=<in.tmp
>test.txt echo %line1%
>>test.txt Echo Test4
for /f "skip=1 delims=" %%a in (in.tmp) do >>test.txt Echo %%a
del in.tmp

将阻止菜单在90%的时间内闪烁..仍然不够好,但在找到合适的解决方案之前,这是另一种解决方法。

答案 2 :(得分:2)

嘿伙计们花了一个小时后,我找到了%100成功率的肮脏解决方案。

逻辑是;检测UIMenuController何时显示并更新它。

在你的ViewController(包含WKWebView)中,像这样在viewDidLoad()中添加UIMenuControllerDidShowMenu观察者;

override func viewDidLoad() {
super.viewDidLoad()
       NotificationCenter.default.addObserver(
                         self,
                         selector: #selector(uiMenuViewControllerDidShowMenu),
                         name: NSNotification.Name.UIMenuControllerDidShowMenu,
                         object: nil)
}

不要忘记删除deinit中的观察者。

    deinit {
    NotificationCenter.default.removeObserver(
                       self,
                       name: NSNotification.Name.UIMenuControllerDidShowMenu,
                       object: nil)
    }

在你的选择器中,像这样更新UIMenuController:

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.
        }
    }

在调用UIMenuController的ViewController中,将调用此方法。我正在开发浏览器应用程序,所以我也有searchBar,用户可能想要将文本粘贴到那里。因为我在我的webview中检测到longPress并检查UIMenuController是否被WKWebView召唤。

此解决方案的行为与gif相同。你可以看一秒菜单,但你不能点击它。你可以尝试在它消失之前点击它,但你不会成功。请尝试告诉我你的结果。

我希望它有所帮助。

干杯。

enter image description here

答案 3 :(得分:2)

此错误实际上是由WKContentView中添加的操作引起的,WKContentView是一个私有类。你可以添加一个UIView扩展来解决它:

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
    }
}

答案 4 :(得分:2)

我尝试了Stephan Heilner的解决方案,但未在Swift 4中编译。

这是我的实现,用于在与Swift 4兼容的WKWebView中禁用menuController。

在我的WKWebView子类中,添加了以下属性和功能:

var wkContentView: UIView? {
    return self.subviewWithClassName("WKContentView")
}


private func swizzleResponderChainAction() {
    wkContentView?.swizzlePerformAction()
}

然后,我在同一文件中添加了一个扩展名,但不在WKWebView子类中:

// 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
    }
}

最后,我从初始化程序中调用了swizzleResponderChainAction()函数(您可以覆盖指定的初始化程序,也可以创建一个方便的初始化程序):

override init(frame: CGRect, configuration: WKWebViewConfiguration) {
    super.init(frame: frame, configuration: configuration)

    swizzleResponderChainAction()
}

现在,使用UIMenuController时,WKWebView不再崩溃。

答案 5 :(得分:1)

经过一番观察,我修好了它。

-canPerformAction:withSender:我正在为NO_share选项返回_define,因为我在项目中并不需要它们。它在第一次选择单词时按预期工作,但从第二次显示选项。

简单修复:在[self becomeFirstResponder];或触摸委托方法

中添加tapGuesture
-(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//
}

答案 6 :(得分:1)

这是我的最终解决方案,改编自此处发布的解决方案。关键是要听取UIMenuControllerWillShowMenu通知,然后Dispatch.main.async来隐藏菜单。这似乎可以避免闪烁的菜单。

我的示例使用UITextField,但应该很容易适应WKWebView

class NoMenuTextField: UITextField {

    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        if superview == nil {
            deregisterForMenuNotifications()
        } else {
            registerForMenuNotifications()
        }
    }

    func registerForMenuNotifications() {
        NotificationCenter.default.addObserver(forName: Notification.Name.UIMenuControllerWillShowMenu,
                                               object: nil,
                                               queue: OperationQueue.main)
        { _ in
            DispatchQueue.main.async {
                UIMenuController.shared.setMenuVisible(false, animated: false)
                UIMenuController.shared.update()
            }
        }
    }

    func deregisterForMenuNotifications() {
        NotificationCenter.default.removeObserver(self,
                                                  name: Notification.Name.UIMenuControllerWillShowMenu,
                                                  object: nil)
    }
}

答案 7 :(得分:0)

我使用的一种方法是简单地使用CSS禁用菜单。 CSS属性称为-webkit-touch-callout: none;。您可以将它应用于顶级元素并为整个页面或任何子元素禁用它,并以更高的精度禁用它。希望有所帮助。

答案 8 :(得分:0)

pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
    // Add:
    // Disable LongPress and Selection, no more UIMenucontroller
    [self.wkWebView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none'" completionHandler:nil];
    [self.wkWebView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none'" completionHandler:nil]; }

答案 9 :(得分:0)

在iOS 11中,我通过WKWebView的扩展找到了一个简单的解决方案。我还没有检查这是否可以在iOS的早期版本中使用。以下是一个菜单项的简单示例。

import UIKit
import WebKit

extension WKWebView {

    override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        switch action {
        case #selector(highlightHandler):
            return true
        default:
            return false
        }
    }

    func createEditMenu() { // Should be called once
        let highlight = UIMenuItem(title: "Highlight", action: #selector(highlightHandler))
        menuItems.append(highlight)
        UIMenuController.shared.menuItems = [highlight]
    }

    @objc func highlightHandler(sender: UIMenuItem) {
        print("highlight clicked")
    }
}

答案 10 :(得分:0)

子类WKWebView并重写canPerformAction以返回false:

class WebView : WKWebView {
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return false
    }
}

答案 11 :(得分:0)

Swift 5.2

NotificationCenter.default.addObserver(self, selector: #selector(willShowMenu(_:)), name: UIMenuController.willShowMenuNotification, object: nil)

...

@objc private func willShowMenu(_ notification: NSNotification) {
    DispatchQueue.main.async {
        UIView.performWithoutAnimation {
            UIMenuController.shared.hideMenu()
        }
    }
}