Отключить полное меню редактирования 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.