在iOS开发中,经常会用到UIWebView/WKWebView来加载Html5。特别是随着Hybrid开发的流行,有的公司直接砍掉大部分的Native开发,转而大量采用H5页面替代Native页面。这就要多研究研究UIWebView/WKWebView了。本文主要针对Hybrid开发中遇到的一个小问题,提供一个解决方案。
当H5中含有<input type=file>标签时,点击选择文件按钮会默认弹出Native的文件选择菜单,包含相机拍照、从相册选择两个选项。这是由于系统对<input type=file>对标签进行了监听,并做了处理。
现在项目中不满足于这两个选项,如果想自己加一个,或者做任何自己想要的定制,就需要捕获这个<input type=file>标签,并在事件里面实现自己想要实现的功能。那么如何捕获这个标签呢?用UIWebView/WKWebView的代理是不行的,没有哪个代理方法会回调这个标签的监听事件。下面提供一种解决方案。
主要思路是,虽然拦截不了js发给Native的通知,但是可以通过Runtime拦截Native弹出窗口,因为知道这个窗口是被present出来的。通过断点,可以看到,对于UIWebView来说,present的的是UIDocumentMenuViewController,并通过其代理UIWebFileUploadPanel完成文件的上传。WKWebView也是类似的。
因此可以通过Runtime来hook出UIViewController的presentViewController方法,拿到将要被present的UIViewController,并判断其类型,如果是UIDocumentMenuViewController类型且其代理为UIWebFileUploadPanel(或者WKFileUploadPanel),将present方法return掉,不让他弹出来;如果不是这种类型的,才让present。如下所示:
- (void)gigi_presentViewController:(UIViewController*)viewControllerToPresent animated:(BOOL)flag completion:(void(^)(void))completion {
//如果present的viewcontroller是UIDocumentMenuViewController类型,且代理是WKFileUploadPanel或UIWebFileUploadPanel进行拦截
if([viewControllerToPresent isKindOfClass:[UIDocumentMenuViewController class]]) {
UIDocumentMenuViewController*dvc = (UIDocumentMenuViewController*)viewControllerToPresent;
if([dvc.delegateisKindOfClass:NSClassFromString(@"WKFileUploadPanel")] || [dvc.delegateisKindOfClass:NSClassFromString(@"UIWebFileUploadPanel")]) {
self.isFileInputIntercept=YES;
dispatch_async(dispatch_get_main_queue(), ^{
[self onFileInputIntercept];
});
return;
}
}
//正常情况下的present
[selfgigi_presentViewController:viewControllerToPresentanimated:flagcompletion:completion];
}
并在return之前执行想要自己实现的代码,做自己想干的事。在这里执行了一个[self onFileInputIntercept]方法,把拦截传递出去。这样就大功告成了吗?不是的。尝试一下,发现,第一次点击时,阻止默认窗口弹出来是可以的,但是,第二次时,就不会调用present方法了,因此就无法进行拦截。
究其原因,发现默认窗口弹出后,在窗口消失时,调用了dismisViewControllerAnimated这个回调,并执行了它的bolck completion。如果执行了这个block,第二次就能够正常拦截;如果不执行block,第二次就无法拦截。这个completion到底是怎么实现的,不得而知,因为是系统内部实现的,看不到源码,但是也不需要知道。只需要知道它是一定要执行的就行。那么怎么来执行这个block呢。没错,可以通过UIDocumentMenuViewController来模拟取消,加上这关键的一句:
[dvc.delegate documentMenuWasCancelled:dvc];
来模拟窗口被取消,从而执行那个至关重要的completion block。那么在dismisViewControllerAnimated也要做一些处理,如下所示:
- (void)gigi_dismissViewControllerAnimated:(BOOL)flag completion:(void(^)(void))completion {
//如果进行了拦截,禁止当前viewcontroller的dismiss
if(self.isFileInputIntercept) {
self.isFileInputIntercept=NO;
completion();
return;
}
//正常情况下viewcontroller的dismiss
[selfgigi_dismissViewControllerAnimated:flagcompletion:^{
if(completion) {
completion();
}
}];
}
至此才大功告成,对<input type=file>进行了有效的拦截。
最后,附上本文的Demo代码地址:https://github.com/frog78/Gigi
参考链接:
http://news.91.com/mip/s594a8f1b155c.html