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

требование

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

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

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

Я знаю, что стандартным способом достижения этого является WKWebView подкласса 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 имеет никакого WillShowMenu, а с DidShowMenu уже слишком поздно.

Эксперименты доступны здесь: https://github.com/dwieringa/WKWebViewEditMenuHidingTest

Что мне не хватает? Есть ли другой способ отключить или скрыть стандартное меню редактирования для WKWebView?

Ответ 1

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

- (BOOL)canResignFirstResponder {
    return NO;
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

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

Ответ 2

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

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

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

}

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

Ответ 3

Эй, ребята, потратив на это несколько часов, я нашел грязное решение со скоростью успеха% 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, этот метод будет вызван. Я разрабатываю приложение для браузера, поэтому у меня есть также searchBar, и пользователь может захотеть вставить туда текст. Из-за этого я обнаруживаю longPress в своем веб-просмотре и проверяю, вызван ли UIMenuController WKWebView.

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

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

Приветствия.

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

Ответ 4

Эта ошибка на самом деле вызвана действиями, добавленными в 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
    }
}

Ответ 5

Я исправил это после некоторых наблюдений.

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

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

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

Ответ 8

знак прагмы - 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

В 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

Я попробовал решение от Stephan Heilner, но оно не скомпилировалось в 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() из инициализатора (вы можете переопределить назначенный инициализатор или создать swizzleResponderChainAction()):

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

    swizzleResponderChainAction()
}

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