18

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)

5
  • I've had a play with your code and there is nothing wrong with the way the cut: method works as long as you don't press your button <input type="button">. If you press the done button on the keyboard or just double tap without dismissing the keyboard it works fine. It's quite confusing.
    – Popeye
    Jan 23, 2014 at 20:50
  • There are many cases where someone inputs text into an input, presses a button, then double-taps the same input, selects the text and cuts it. So I think this is a critical bug. This bug was found by someone testing my app, who happened to perform these steps, and crashed my app.
    – paulvs
    Jan 23, 2014 at 20:57
  • No this isn't a bug for Apple this differently your bug. You need to handle the event for the UIMenuItem that you press, simples.
    – Popeye
    Jan 23, 2014 at 21:16
  • This is an Apple bug. The problem is the cut: is sent incorrectly in the responder chain, and ends up sent to UIWebView instead of UIWebDocumentView.
    – Léo Natan
    Jan 23, 2014 at 21:23
  • @paulvs i am still facing this issue in iOS 12. Did you get solution for this issue? Dec 6, 2018 at 9:02

4 Answers 4

16

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.

11
  • Awesome! For anyone interested just add #import <objc/runtime.h> to the top of the file. I wonder how many apps could be crashed with this bug. Do you think it would be ok/safe to put this in production code? Would it pass the App Store?
    – paulvs
    Jan 24, 2014 at 11:42
  • @PaulVon It should be safe. Nothing in there is specifically private API. Of course, it is better Apple just fixed it.
    – Léo Natan
    Jan 24, 2014 at 11:57
  • 1
    Thanks Leo. The problem seems to be related to that after pressing a button in a UIWebView, subsequent double-taps on any input do not bring up the keyboard (like they do before pressing a button), and the Cut/Paste menu bar is displayed without the keyboard, and this is when a crash occurs if any of the Cut/Paste buttons are pressed. Note: jQuery Mobile using Cordova does not have this problem.
    – paulvs
    Jan 24, 2014 at 12:10
  • 1
    @PaulVon I debugged the jQuery Mobile project, and it seems somehow Javascript catches the tap events and makes the UIWebDocumentView first responder, so by the time the callout bar is presented and the user can select "Cut", the responder chain is correct. Whereas, removing the script elements loading the jQM library, the bug reproduces.
    – Léo Natan
    Jan 25, 2014 at 1:52
  • 1
    @LeoNatan I have similar issue mentioned, so I have tried all given solutions here, however these solutions are failing and could not resolve issue. Apart from this any other solution? Appreciate your support. Dec 7, 2018 at 12:41
5

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)
    }
}
1
  • this worked for me and cut/copy/paste worked every time I tried too! 🕺
    – agandi
    May 17, 2019 at 15:21
2

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);
    }

}
0
- (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];
}
1
  • Code-only answers are discouraged. Please click on edit and add some words summarising how your code addresses the question, or perhaps explain how your answer differs from the previous answer/answers. Thanks
    – Nick
    Dec 17, 2018 at 7:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.