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:
If the user performs a long press before the WKWebView has finished the navigation the default (Safari's) alert controller appears.
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
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.