0x00 需求:长按识别UIWebView中的二维码,如下图
长按识别二维码
0x01 方案1:
给UIWebView增加一个长按手势,激活长按手势时获取当前UIWebView的截图,分析是否包含二维码。
核心代码:略
优点:流程简单,可以快速实现。
不足:无法实现保存UIWebView中图片,如果当前WebView二维码显示不全或者多个二维码,使用这种方式实现的二维码识别也会有问题;
0x02 方案2:
长按UIWebView时,获取手指单击位置的图片的URL地址。这种方案是通过获取手指点击的位置,然后获取该位置的标签的src属性,进而获取到url。
核心代码:
1@interface CVWebViewController ()<UIGestureRecognizerDelegate> 2 3@property (weak, nonatomic) IBOutlet UIWebView *webView; 4 5@end 6 7@implementation CVWebViewController 8 9- (void)viewDidLoad 10{ 11 [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mp.weixin.qq.com/s?__biz=MzI2ODAzODAzMw==&mid=2650057120&idx=2&sn=c875f7d03ea3823e8dcb3dc4d0cff51d&scene=0#wechat_redirect"]]]; 12 UILongPressGestureRecognizer *longPressed = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressed:)]; 13 longPressed.delegate = self; 14 [self.webView addGestureRecognizer:longPressed]; 15} 16 17- (void)longPressed:(UITapGestureRecognizer*)recognizer 18{ 19 if (recognizer.state != UIGestureRecognizerStateBegan) { 20 return; 21 } 22 CGPoint touchPoint = [recognizer locationInView:self.webView]; 23 NSString *js = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y]; 24 NSString *imageUrl = [self.webView stringByEvaluatingJavaScriptFromString:js]; 25 if (imageUrl.length == 0) { 26 return; 27 } 28 NSLog(@"image url:%@",imageUrl); 29 NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; 30 31 UIImage *image = [UIImage imageWithData:data]; 32 if (image) { 33 //...... 34 //save image or Extract QR code 35 } 36} 37 38-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 39{ 40 return YES; 41}
上述代码实现的核心部分就是
1NSString *js = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y]; 2NSString *imageUrl = [self.webView stringByEvaluatingJavaScriptFromString:js];
第一行代码是通过js获取点击位置的标签的src属性;
第二行代码是接受向webview注入第一行的js代码后返回的src属性。
如果点击位置是图片,那么久可以通过img.src拿到图片的url地址,如果不是就返回空值。
效果:
image URL
注意:由于UIWebView内部是有一个ScrollView,默认情况下不支持多个手势的,因此需要实现UIGestureRecognizerDelegate中的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:协议,以支持多个手势(增加长按手势)。
优点:通过识别img标签的url属性,既可以实现保存图片的功能,也可以实现识别图片中二维码的功能;该方案不仅仅可以获取img标签的属性,也可以根据需要获取其他标签,例如链接标签a的属性(需调整部分代码,识别tagName)。
不足:每次获取图片,都需要根据url获取,相当于从网络获取,万一图片太大或者网络不好,势必会影响用户体验,方案3中会介绍如何从缓存中获取image数据。
0x03 方案3:
利用Runtime,动态地为UIWebView注入一段js代码,获取IMG标签的src属性,然后从UIWebView的缓存中获取image数据。
webview加载完成图片完成之后,图片数据已经缓存在webview里了,只需找到从缓存中获取这些数据的方法。
方案3使用的从缓存中获取image数据原理是:使用NSURLProtocol,webview在处理请求的过程中会调用
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
NSURLProtocol把webView请求返回来的data用压缩的方式的存储在cache的文件夹下, 发出请求的时候会先去读取缓存。
在github上找到了一个RNCachingURLProtocol,可以方便地从缓存中获取数据。
关于NSURLProtocol,能够让你去重新定义苹果的URL加载系统 (URL Loading System)的行为,URL Loading System里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。
核心代码:
1static NSString *const kTouchJavaScriptString = 2 @"document.ontouchstart=function(event){\ 3 x=event.targetTouches[0].clientX;\ 4 y=event.targetTouches[0].clientY;\ 5 document.location=\"myweb:touch:start:\"+x+\":\"+y;};\ 6 document.ontouchmove=function(event){\ 7 x=event.targetTouches[0].clientX;\ 8 y=event.targetTouches[0].clientY;\ 9 document.location=\"myweb:touch:move:\"+x+\":\"+y;};\ 10 document.ontouchcancel=function(event){\ 11 document.location=\"myweb:touch:cancel\";};\ 12 document.ontouchend=function(event){\ 13 document.location=\"myweb:touch:end\";};"; 14 15static NSString *const kImageJS = @"keyForImageJS"; 16static NSString *const kImage = @"keyForImage"; 17static NSString *const kImageQRString = @"keyForQR"; 18 19static const NSTimeInterval KLongGestureInterval = 0.8f; 20...... 21 SwizzlingMethod([self class], @selector(webViewDidStartLoad:), @selector(sl_webViewDidStartLoad:)); 22 SwizzlingMethod([self class], @selector(webView:shouldStartLoadWithRequest:navigationType:), @selector(sl_webView:shouldStartLoadWithRequest:navigationType:)); 23 SwizzlingMethod([self class], @selector(webViewDidFinishLoad:), @selector(sl_webViewDidFinishLoad:)); 24...... 25- (void)sl_webViewDidStartLoad:(UIWebView *)webView 26{ 27 //Add long press gresture for web view 28 UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; 29 longPress.minimumPressDuration = KLongGestureInterval; 30 longPress.delegate = self; 31 [self.webView addGestureRecognizer:longPress]; 32 33 [self sl_webViewDidStartLoad:webView]; 34} 35 36- (BOOL)sl_webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 37{ 38 NSString *requestString = [[request URL] absoluteString]; 39 40 NSArray *components = [requestString componentsSeparatedByString:@":"]; 41 42 if ([components count] > 1 && [(NSString *)[components objectAtIndex:0] isEqualToString:@"myweb"]) { 43 44 if([(NSString *)[components objectAtIndex:1] isEqualToString:@"touch"]) { 45 46 if ([(NSString *)[components objectAtIndex:2] isEqualToString:@"start"]) { 47 48 NSLog(@"touch start!"); 49 50 float pointX = [[components objectAtIndex:3] floatValue]; 51 float pointY = [[components objectAtIndex:4] floatValue]; 52 53 NSLog(@"touch point (%f, %f)", pointX, pointY); 54 55 NSString *js = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).tagName", pointX, pointY]; 56 57 NSString * tagName = [self.webView stringByEvaluatingJavaScriptFromString:js]; 58 59 self.imageJS = nil; 60 if ([tagName isEqualToString:@"IMG"]) { 61 62 self.imageJS = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", pointX, pointY]; 63 64 } 65 66 } else { 67 68 if ([(NSString *)[components objectAtIndex:2] isEqualToString:@"move"]) { 69 NSLog(@"you are move"); 70 } else { 71 if ([(NSString *)[components objectAtIndex:2] isEqualToString:@"end"]) { 72 NSLog(@"touch end"); 73 } 74 } 75 } 76 } 77 78 if (self.imageJS) { 79 NSLog(@"touching image"); 80 } 81 82 return NO; 83 } 84 85 return [self sl_webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; 86} 87- (void)sl_webViewDidFinishLoad:(UIWebView *)webView 88{ 89 //inject js 90 [webView stringByEvaluatingJavaScriptFromString:kTouchJavaScriptString]; 91 92 [self sl_webViewDidFinishLoad:webView]; 93} 94...... 95- (void)handleLongPress:(UILongPressGestureRecognizer *)sender 96{ 97 if (sender.state != UIGestureRecognizerStateBegan) { 98 return; 99 } 100 101 NSString *imageUrl = [self.webView stringByEvaluatingJavaScriptFromString:self.imageJS]; 102 103 if (imageUrl) { 104 105 NSData *data = nil; 106 NSString *fileName = [RNCachingURLProtocol cachePathForURLString:imageUrl]; 107 108 RNCachedData *cache = [NSKeyedUnarchiver unarchiveObjectWithFile:fileName]; 109 110 if (cache) { 111 NSLog(@"read from cache"); 112 data = cache.data; 113 } else{ 114 NSLog(@"read from url"); 115 data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; 116 } 117 118 UIImage *image = [UIImage imageWithData:data]; 119 if (!image) { 120 NSLog(@"read fail"); 121 return; 122 } 123 self.image = image; 124 125 FSActionSheet *actionSheet = nil; 126 127 if ([self isAvailableQRcodeIn:image]) { 128 129 actionSheet = [[FSActionSheet alloc] initWithTitle:nil 130 delegate:self 131 cancelButtonTitle:@"Cancel" 132 highlightedButtonTitle:nil 133 otherButtonTitles:@[@"Save Image", @"Extract QR code"]]; 134 135 } else { 136 137 actionSheet = [[FSActionSheet alloc] initWithTitle:nil 138 delegate:self 139 cancelButtonTitle:@"Cancel" 140 highlightedButtonTitle:nil 141 otherButtonTitles:@[@"Save Image"]]; 142 } 143 [actionSheet show]; 144 145 } 146} 147...... 148- (BOOL)isAvailableQRcodeIn:(UIImage *)img 149{ 150 if (iOS7_OR_EARLY) { 151 return NO; 152 } 153 154 //Extract QR code by screenshot 155 //UIImage *image = [self snapshot:self.view]; 156 157 // IF image is a full qr code, CIDetector can not detect qr string, I am not sure why. 158 UIImage *image = [self imageByInsetEdge:UIEdgeInsetsMake(-20, -20, -20, -20) withColor:[UIColor lightGrayColor] withImage:img]; 159 160 CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{}]; 161 162 NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image.CGImage]]; 163 164 if (features.count >= 1) { 165 CIQRCodeFeature *feature = [features objectAtIndex:0]; 166 167 self.qrCodeString = [feature.messageString copy]; 168 169 NSLog(@"QR result :%@", self.qrCodeString); 170 171 return YES; 172 } else { 173 NSLog(@"No QR"); 174 return NO; 175 } 176} 177...... 178
效果:
图片中不包含二维码
图片中包含二维码
优点:充分利用了缓存,提高了用户体验。
不足:实现略复杂。
0x04 思考
如果是非IMG标签提供的图片,例如div的background image,该如何获取和保存?
WKWebView上的实现。
0x05 Demo地址
https://github.com/guoxiucai/WebViewLongPress