Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WKWebView custom long press menu works but with some major issues

When the user long presses a link an alert controller appears with the options:

  • Open
  • Open in New Tab
  • Copy

There are two problems currently:

  1. If the user performs a long press before the WKWebView has finished the navigation the default (Safari's) alert controller appears.

  2. If the user lifts his finger after the popup animation occurs somehow the WKWebView registers it as a tap and navigates to that link while the alert controller is still displayed on screen.

There are three parts to this mechanism.

Firstly,

After the WKWebView has finished the navigation a javascript is injected to the page that disables the default alert controller.

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    [_webView evaluateJavaScript:@"document.body.style.webkitTouchCallout='none';"
               completionHandler:^(id result, NSError *error){

                   NSLog(@"Javascript: {%@, %@}", result, error.description);
               }];
}

Secondly,

A UILongPressGestureRecognizer is added to the WKWebView and implemented so that it finds the attributes of the elements based on the location of the touch.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

- (void)longPress:(UILongPressGestureRecognizer *)longPressGestureRecognizer
{
    if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {

        _shouldCancelNavigation = YES;

        CGPoint touchLocation = [longPressGestureRecognizer locationInView:_webView];

        NSString *javascript = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Javascript" ofType:@"js"]
                                                         encoding:NSUTF8StringEncoding
                                                            error:nil];

        [_webView evaluateJavaScript:javascript
                   completionHandler:^(id result, NSError *error){

                       NSLog(@"Javascript: {%@, %@}", result, error.description);
                   }];

        [_webView evaluateJavaScript:[NSString stringWithFormat:@"MyAppGetHTMLElementsAtPoint(%f,%f);", touchLocation.x, touchLocation.y]
                   completionHandler:^(id result, NSError *error){

                       NSLog(@"Javascript: {%@, %@}", result, error.description);

                       NSString *tags = (NSString *)result;

                       if ([tags containsString:@",A,"]) {

                           [_webView evaluateJavaScript:[NSString stringWithFormat:@"MyAppGetHREFAttributeAtPoint(%f,%f);", touchLocation.x, touchLocation.y]
                                      completionHandler:^(id result, NSError *error){

                                          NSLog(@"Javascript: {%@, %@}", result, error.description);

                                          NSString *urlString = (NSString *)result;

                                          [_delegate webView:self didLongPressAtTouchLocation:touchLocation URL:[NSURL URLWithString:urlString]];
                                      }];

                           return;
                       }

                       if ([tags containsString:@",IMG,"]) {

                           [_webView evaluateJavaScript:[NSString stringWithFormat:@"MyAppGetSRCAttributeAtPoint(%f,%f);", touchLocation.x, touchLocation.y]
                                      completionHandler:^(id result, NSError *error){

                                          NSLog(@"Javascript: {%@, %@}", result, error.description);

                                          NSString *urlString = (NSString *)result;

                                          [_delegate webView:self didLongPressAtTouchLocation:touchLocation imageWithSourceURL:[NSURL URLWithString:urlString]];
                                      }];

                           return;
                       }
                   }];
    }
}

Lastly,

The delegate method that presents the alert controller is implemented on the main ViewController.

My solution to the second problem has been to add a boolean value shouldCancelNavigation that is YES when the alert controller has been presented and NO when it has been dismissed.

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    if (_shouldCancelNavigation) {

        decisionHandler(WKNavigationActionPolicyCancel);
    }
    else {

        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

Interestingly enough there are many examples on the web where links DO NOT require a policy decision. They just happen with no way for me to stop them.

Example: http://www.dribbble.com

enter image description here

Source: http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/comment-page-3/

Source 2: https://github.com/mozilla-mobile/firefox-ios/pull/61

EDIT:

This solves the 2nd problem but I'm not sure it won't break something somewhere else.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {

        otherGestureRecognizer.enabled = NO;

        otherGestureRecognizer.enabled = YES;
    }

    return YES;
}

EDIT 2:

It does actually create a problem... You can no longer select text since the code above resets the internal long press gesture recognizers.

EDIT 3:

If I remove my implementation completely (all 3 steps) and let the default alert controller kick in every time I long press a link the 2nd problem gets solved.

There's something about Apple's alert controller that prevents the WKWebView from navigating after you lift your finger.

like image 234
Vulkan Avatar asked Mar 27 '17 18:03

Vulkan


People also ask

How does WKWebView work?

Overview. A WKWebView object is a platform-native view that you use to incorporate web content seamlessly into your app's UI. A web view supports a full web-browsing experience, and presents HTML, CSS, and JavaScript content alongside your app's native views.

How can I clear the contents of a UIWebView WKWebView?

To clear old contents of webview With UIWebView you would use UIWebViewDelegate 's - webViewDidFinishLoad: .


2 Answers

This answer will solve the second problem but I'm not sure if it is safe for App Store.

First you will need to break the internal long press gesture recognizers so they do not fire when the user lifts his finger.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {

        if (otherGestureRecognizer.state == UIGestureRecognizerStateBegan) {

            // Warning: This will break how WKWebView handles selection of text.

            [otherGestureRecognizer requireGestureRecognizerToFail:gestureRecognizer];
        }
    }

    return YES;
}

After the user has finished interacting with the custom long press menu this code will fix the broken WKWebView:

    [_webView removeGestureRecognizer:_longPressGestureRecognizer]; // This code will remove the dependency and recover the lost functionality.

    _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];

    _longPressGestureRecognizer.numberOfTouchesRequired = 1;

    _longPressGestureRecognizer.delegate = self;

    [_webView addGestureRecognizer:_longPressGestureRecognizer];

It is such a HACK.

like image 33
Vulkan Avatar answered Oct 06 '22 01:10

Vulkan


If I am not wrong, In the second part :

- (void)longPress:(UILongPressGestureRecognizer *)longPressGestureRecognizer
{
    if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {

//Rest of your code ...
    }
}

You are injecting javascript to disable system dialog. Now after press ends, WKWebview already recieved event triggered from the web link. Since it is too late, why don't you try to check condition for longPressGestureRecognizer.state is equal to UIGestureRecognizerStateEnded instead.

Hence it changes to below code.

    if (longPressGestureRecognizer.state == UIGestureRecognizerStateEnded) {

       //Rest of your code ...
    }

I have not tested this code yet. Would be happier if it works.

like image 136
byJeevan Avatar answered Oct 05 '22 23:10

byJeevan