UIWebView Bug: -[UIWebView cut:]: unrecognized selector sent to instance

一笑奈何 提交于 2019-12-20 10:28:47

问题


In the UIWebView, if an input element containing text has focus, and a button is pressed that causes the input to lose focus, then subsequently double-tapping on the input to regain focus and selecting Cut (or Copy or Paste) from the popup bar that appears causes the UIWebView to crash with the error:

-[UIWebView cut:]: unrecognized selector sent to instance 0x10900ca60

Demo project: https://github.com/guarani/WebViewDoubleTapTestTests.git

I think this must be a UIWebView bug, any ideas?

For completeness, here are the contents of my web view,

<html>
    <head>
    </head>
    <body>
        <br><br>
        <input type="text">
        <input type="button">
    </body>
</html>

Filed a Bug Report at Apple: 15894403

Update 2019/05/30: Bug still present in iOS 12.0 (16E226)


回答1:


This is an Apple bug. The problem is the cut: action is sent incorrectly in the responder chain, and ends up being sent to the UIWebView instance instead of the internal UIWebDocumentView, which implements the method.

Until Apple fixes the bug, let's have some fun with the Objective C runtime.

Here, I subclass UIWebView with the purpose of supporting all UIResponderStandardEditActions methods, by forwarding them to the correct internal instance.

@import ObjectiveC;    

@interface CutCopyPasteFixedWebView : UIWebView @end

@implementation CutCopyPasteFixedWebView

- (UIView*)_internalView
{
    UIView* internalView = objc_getAssociatedObject(self, "__internal_view_key");

    if(internalView == nil && self.subviews.count > 0)
    {
        for (UIView* view in self.scrollView.subviews) {
            if([view.class.description hasPrefix:@"UIWeb"])
            {
                internalView = view;

                objc_setAssociatedObject(self, "__internal_view_key", view, OBJC_ASSOCIATION_ASSIGN);

                break;
            }
        }
    }

    return internalView;
}

void webView_implement_UIResponderStandardEditActions(id self, SEL selector, id param)
{
    void (*method)(id, SEL, id) = (void(*)(id, SEL, id))[[self _internalView] methodForSelector:selector];

    //Call internal implementation.
    method([self _internalView], selector, param);
}

- (void)_prepareForNoCrashes
{
    NSArray* selectors = @[@"cut:", @"copy:", @"paste:", @"select:", @"selectAll:", @"delete:", @"makeTextWritingDirectionLeftToRight:", @"makeTextWritingDirectionRightToLeft:", @"toggleBoldface:", @"toggleItalics:", @"toggleUnderline:", @"increaseSize:", @"decreaseSize:"];

    for (NSString* selName in selectors)
    {
        SEL selector = NSSelectorFromString(selName);

        //This is safe, the method will fail if there is already an implementation.
        class_addMethod(self.class, selector, (IMP)webView_implement_UIResponderStandardEditActions, "");
    }
}

- (void)awakeFromNib
{
    [self _prepareForNoCrashes];

    [super awakeFromNib];
}

@end

Use this subclass in your storyboard.

Have fun.




回答2:


If you don't mind that there is no callout for cut/paste/etc. in the case, when the UIWebview is wrongly becoming first responder, then you can also fix it with this category. This does not prohibit cut/paste/etc. when the UIWebDocumentView (correctly) becomes first responder.

@implementation UIWebView (NoWrongPerformWebview)

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    return NO;
}

@end

// Swift 4 compliant version

import UIKit

extension UIWebView {

    override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        // Should be respond to a certain Selector ??
        return responds(to: action)
    }
}



回答3:


If anyone is interested, here's the swift version of Leo Natans method :

import Foundation
import ObjectiveC

var AssociatedObjectHandle: UInt8 = 0


class CustomWebView: UIWebView {
    func _internalView() -> UIView? {
        var internalView:UIView? = objc_getAssociatedObject(self, "__internal_view_key") as? UIView
        if internalView == nil && self.subviews.count > 0 {
            for view: UIView in self.scrollView.subviews {
                if view.self.description.hasPrefix("UIWeb") {
                    internalView = view
                    objc_setAssociatedObject(self, "__internal_view_key", view, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
                }
            }
        }
        return internalView
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        self._prepareForNoCrashes()
    }

    func _prepareForNoCrashes() {
        let selectors = ["cut:", "copy:", "paste:", "select:", "selectAll:", "delete:", "makeTextWritingDirectionLeftToRight:", "makeTextWritingDirectionRightToLeft:", "toggleBoldface:", "toggleItalics:", "toggleUnderline:", "increaseSize:", "decreaseSize:"]
        for selName: String in selectors {
            let selector = NSSelectorFromString(selName)
            //This is safe, the method will fail if there is already an implementation.
            let swizzledMethod:IMP = class_getInstanceMethod(CustomWebView.self, #selector(CustomWebView.webView_implement_UIResponderStandardEditActions))
            class_addMethod(CustomWebView.self, selector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
        }
    }

    func webView_implement_UIResponderStandardEditActions(this:AnyObject, selector:Selector, param:AnyObject)
    {
        let method = {(val1: UIView?, val2: Selector, val3: AnyObject) -> Void in
            self._internalView()?.methodForSelector(selector)
        }

        method(self._internalView(), selector, param);
    }

}



回答4:


- (UIView *)_internalView {
    UIView *internalView = nil;

    if (internalView == nil && self.subviews.count > 0) {
        for (UIView *view in self.scrollView.subviews) {
            if([view.class.description hasPrefix:@"UIWeb"]) {
                internalView = view;
                break;
            }
        }
    }

    return internalView;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    struct objc_method_description methodDescription = protocol_getMethodDescription(@protocol(UIResponderStandardEditActions), aSelector, NO, YES);

    if (methodDescription.name == aSelector) {
        UIView *view = [self _internalView];
        if ([view respondsToSelector:aSelector]) {
            return view;
        }
    }
    return [super forwardingTargetForSelector:aSelector];
}


来源:https://stackoverflow.com/questions/21316784/uiwebview-bug-uiwebview-cut-unrecognized-selector-sent-to-instance

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!