UIWebView/WKWebView對標籤攔截

weixin_33782386發表於2017-07-28

       在iOS開發中,經常會用到UIWebView/WKWebView來載入Html5。特別是隨著Hybrid開發的流行,有的公司直接砍掉大部分的Native開發,轉而大量採用H5頁面替代Native頁面。這就要多研究研究UIWebView/WKWebView了。本文主要針對Hybrid開發中遇到的一個小問題,提供一個解決方案。

        當H5中含有<input type=file>標籤時,點選選擇檔案按鈕會預設彈出Native的檔案選擇選單,包含相機拍照、從相簿選擇兩個選項。這是由於系統對<input type=file>對標籤進行了監聽,並做了處理。

3551920-805f831e8f15021a.png
Html5檔案
3551920-4a83bd87eae2e600.png
預設彈出視窗

        現在專案中不滿足於這兩個選項,如果想自己加一個,或者做任何自己想要的定製,就需要捕獲這個<input type=file>標籤,並在事件裡面實現自己想要實現的功能。那麼如何捕獲這個標籤呢?用UIWebView/WKWebView的代理是不行的,沒有哪個代理方法會回撥這個標籤的監聽事件。下面提供一種解決方案。

      主要思路是,雖然攔截不了js發給Native的通知,但是可以通過Runtime攔截Native彈出視窗,因為知道這個視窗是被present出來的。通過斷點,可以看到,對於UIWebView來說,present的的是UIDocumentMenuViewController,並通過其代理UIWebFileUploadPanel完成檔案的上傳。WKWebView也是類似的。

3551920-ca9f723e9a6bd28e.png
斷點檢視方法呼叫

        因此可以通過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


相關文章