web-dev-qa-db-ja.com

WKWebViewでUIMenuController編集メニュー全体を無効にする

要件

WKWebViewがあり、編集メニューからシステムメニュー項目(コピー、定義、共有...)を削除して、自分の項目を表示したいと思います。

私はiOS8と9をターゲットにしています。現在、Xcode 7.0.1シミュレーター(iOS 9)とiOS9.0.2を実行しているiPhone6でテストしています。

標準的な方法が機能しない

これを実現する標準的な方法は、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:
_

計画された回避策

私の望みは、編集メニューを完全に非表示にして、 QBPopupMen を使用してカスタムメニューに置き換えることです。

私の問題は、標準の[編集]メニューを非表示または無効にする方法を見つけることができなかったことです。 UIMenuControllerWillShowMenuNotificationの_[UIMenuController sharedMenuController].menuVisible = NO;_で非表示にする提案をいくつか見つけましたが、これを機能させることができませんでした。 WillShowMenuには影響しません。 DidShowMenuで非表示にすることはできますが、その時点では手遅れで、メニューが点滅します。

また、[[UIMenuController sharedMenuController] setTargetRect:CGRectMake(0, 0, 1, 1) inView:self.extraView];を使用して表示領域の外側に配置しようとしましたが、WillShowMenuを使用しても効果がなく、DidShowMenuを使用すると手遅れになります。

ここで利用可能な実験: https://github.com/dwieringa/WKWebViewEditMenuHidingTest

何が足りないのですか? WKWebViewの標準編集メニューを無効または非表示にする別の方法はありますか?

18
davew

あなたの回避策に基づいて、私はそれを見つけました:

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

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

}

メニューが90%の回数点滅するのを防ぎます。それでも十分ではありませんが、適切な解決策を見つける前の別の回避策です。

6
Paulo Cesar

ビューコントローラをファーストレスポンダーにして、ファーストレスポンダーを辞任しないようにしてください

- (BOOL)canResignFirstResponder {
    return NO;
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

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

6
ranunez

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がクラッシュしなくなりました。

2
Frédéric Adda

何時間も費やした後、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もあり、ユーザーはそこにテキストを貼り付けたいと思うかもしれません。そのため、WebビューでlongPressを検出し、UIMenuControllerがWKWebViewによって呼び出されるかどうかを確認します。

このソリューションはgifのように動作します。メニューは一瞬表示されますが、タップすることはできません。フェードアウトする前にタップしてみることができますが、成功しません。結果を教えてください。

私はそれが誰かを助けることを願っています。

乾杯。

enter image description here

2
ysnzlcn

このバグは、実際には、プライベートクラスである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
    }
}
2
Stephan Heilner

少し観察して修正しました。

-canPerformAction:withSender:で、_shareおよび_defineオプションに対してNOを返します。これは、プロジェクトでそれらを必要としないためです。初めてWordを選択すると期待どおりに機能しますが、2回目からオプションが表示されます。

簡単な修正:tapGuesture[self becomeFirstResponder];を追加するか、デリゲートメソッドをタッチします

-(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//
}
1
Lax

これが私の最終的な解決策であり、ここに投稿された解決策を応用したものです。重要なのは、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)
    }
}
1
LewisJi

IOS 11では、WKWebViewの拡張による簡単な解決策を見つけました。これが以前のバージョンのiOSで機能するかどうかは確認していません。以下は、1つのメニュー項目を持つ簡単な例です。

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")
    }
}
0
Gary Norris

私が使用した1つの方法は、CSSを使用してメニューを無効にすることです。 CSSプロパティは -webkit-touch-callout: none; 。これを最上位の要素に適用して、ページ全体または任意の子要素に対して無効にし、より正確に無効にすることができます。お役に立てば幸いです。

0
Ryan

プラグママーク-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]; }
0
Rome