Отключить все меню редактирования UIMenuController в WKWebView

Требование

У меня есть WKWebView, и я хотел бы удалить элементы системного меню (Копировать, Определить, Поделиться...) из меню «Правка» и представить свои собственные.

Я нацелен на iOS 8 и 9. В настоящее время я тестирую симулятор Xcode 7.0.1 (iOS 9) и мой iPhone 6 под управлением iOS 9.0.2.

Стандартный метод не работает

Я знаю, что стандартный способ добиться этого — создать подкласс WKWebView и реализовать -canPerformAction:withSender:. Однако я обнаружил, что с WKWebView -canPerformAction:withSender: не вызывается для действий copy: или define:. Похоже, это известная ошибка (WKWebView и 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?


person davew    schedule 20.10.2015    source источник
comment
Из любопытства вы отправили отчет об ошибке в Apple по этому поводу?   -  person Ryan    schedule 19.11.2015
comment
@ Райан, нет еще. Спасибо за ваш ответ ниже. Я только что попробовал, и я все еще вижу меню, когда я долго нажимаю на текст в iOS 9 на моем iPhone 6. Вы пробовали это с WKWebView на iOS? Я проверил с помощью Web Inspector, что новый параметр CSS применяется к телу.   -  person davew    schedule 19.11.2015
comment
@davew есть успехи в этом?   -  person Jed Grant    schedule 29.11.2015
comment
@JedGrant нет, пока я живу со стандартным меню редактирования. 19 ноября я ненадолго опробовал предложение Райана, но оно не сработало, и у меня не было времени копнуть глубже.   -  person davew    schedule 02.12.2015
comment
iOS 13 beta 1 исправляет это!   -  person Tom Hamming    schedule 10.06.2019


Ответы (12)


Основываясь на вашем обходном пути, я узнал, что:

-(void)menuWillShow:(NSNotification *)notification
{
    NSLog(@"MENU WILL SHOW");

    dispatch_async(dispatch_get_main_queue(), ^{
        [[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
    });

}

Предотвратит мигание меню в 90% случаев. Все еще недостаточно хорошо, но это еще один обходной путь, прежде чем мы найдем достойное решение.

person Paulo Cesar    schedule 11.12.2015
comment
Я в порядке с этим. +1 - person Ahmed Khalaf; 24.10.2017
comment
Не уверен, изменилось ли это с тех пор, как вы опубликовали, но для меня это работает (например, меню никогда не отображается) в 100% случаев после не менее 30 попыток :) - person André Morujão; 09.01.2018
comment
Я даже больше не работаю над этим проектом, но это приятно знать :) - person Paulo Cesar; 10.01.2018

Попробуйте сделать ваш контроллер представления первым ответчиком и не дать ему отказаться от первого ответчика.

- (BOOL)canResignFirstResponder {
    return NO;
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

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

person ranunez    schedule 07.02.2016
comment
Это правильный ответ. Я использовал то же самое в Swift 3, xcode 8.2 и работал нормально. Вот быстрый код. переопределить var canBecomeFirstResponder: Bool { return true } переопределить var canResignFirstResponder: Bool { return false } - person Vijay; 13.01.2017
comment
Это проблематично, если у вас есть ввод текста в WKWebView. - person André Morujão; 19.12.2017

Я исправил это после некоторого наблюдения.

В -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//
}
person Lax    schedule 14.01.2016

Привет, ребята, потратив на это несколько часов, я нашел грязное решение с вероятностью успеха 100%.

Логика есть; определить, когда UIMenuController отображался, и обновить его.

В вашем ViewController (содержащем WKWebView) добавьте наблюдателя UIMenuControllerDidShowMenu в viewDidLoad() следующим образом;

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

В вашем ViewController, который когда-либо вызывает UIMenuController, будет вызван этот метод. Я разрабатываю приложение для браузера, поэтому у меня также есть панель поиска, и пользователь может захотеть вставить туда текст. Из-за этого я обнаруживаю longPress в своем веб-просмотре и проверяю, вызывается ли UIMenuController WKWebView.

Это решение будет вести себя как в gif. Вы можете увидеть меню на секунду, но вы не можете нажать на него. Вы можете попытаться коснуться его, прежде чем он исчезнет, ​​но у вас ничего не получится. Пожалуйста, попробуйте и сообщите мне ваши результаты.

Я надеюсь, что это поможет кому-то.

Ваше здоровье.

введите здесь описание изображения

person ysnzlcn    schedule 28.02.2017
comment
привет, как мы можем сделать это в объективе c? - person coder; 23.11.2017
comment
Решение Пауло Сезара не показывает меню в течение этой доли секунды: stackoverflow.com/a/34228687/374516 - person André Morujão; 09.01.2018

Эта ошибка на самом деле вызвана добавлением действий в 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
    }
}
person Stephan Heilner    schedule 23.03.2017
comment
Это решение не компилируется в Swift 4. Я получаю следующее сообщение об ошибке: Метод «initialize()» определяет метод «initialize» класса Objective-C, что не разрешено Swift - person Frédéric Adda; 06.03.2018
comment
@FrédéricAdda, вы нашли какое-нибудь решение? относительно метода инициализации(). - person Srini; 08.01.2019

Я попробовал решение Стефана Хейлнера, но оно не скомпилировалось в Swift 4.

Это моя реализация для отключения menuController в WKWebView, который работает со Swift 4.

В моем подклассе 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()
}

Теперь WKWebView больше не падает при использовании UIMenuController.

person Frédéric Adda    schedule 08.01.2019
comment
Я попробовал ваше решение в своем коде, но UIMenuController не отключен. Тем не менее он появляется при выборе. Также метод swizzleMethod не вызывается. - person Srini; 25.01.2019
comment
Извините, я забыл уточнить, что вы должны вызывать новую функцию swizzled из инициализатора. Теперь это исправлено. Спасибо! - person Frédéric Adda; 26.01.2019

Вот мое окончательное решение, адаптированное из решений, размещенных здесь. Ключ в том, чтобы прослушать уведомление 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)
    }
}
person LewisJi    schedule 22.04.2018

Один из способов, который я использовал, — просто отключить меню с помощью CSS. Свойство CSS называется -webkit-touch-callout: none;. Вы можете применить его к элементу верхнего уровня и отключить его для всей страницы или любого дочернего элемента и отключить его с большей точностью. Надеюсь, это поможет.

person Ryan    schedule 19.11.2015

метка прагмы — 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]; }
person Rome    schedule 29.11.2017

В 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")
    }
}
person Gary Norris    schedule 16.11.2018

Подкласс WKWebView и переопределение canPerformAction для возврата false:

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

Свифт 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()
        }
    }
}
person Steve Ham    schedule 29.11.2020