WebKit的使用

Web view 用於加載和顯示豐富的網絡內容。例如,嵌入 HTML 和網站。Mail app 使用 web view 顯示郵件中的 HTML 內容。

iOS 8 和 macOS 10.10 中引入了WebKit framework,用以取代UIWebViewWebView。同時,在兩個平臺上提供同一API。與UIWebView相比,WebKit 有以下優勢:使用了與 Safari 一樣的 JavaScript engine,在運行腳本前,將腳本編譯爲機器代碼,速度更快;支持多進程架構,Web 內容在單獨的線程中運行,WKWebView崩潰不會影響 app運行;能夠以60fps滑動。另外,在 iOS 12 和 macOS 10.14中,UIWebViewWebView已經被正式棄用。

WebKit framework 提供了多個類和協議,用於在窗口中顯示網絡內容,並實現類似瀏覽器功能。例如,點擊鏈接時顯示鏈接內容,維護前進、後退列表,維護最近訪問列表。加載網頁內容時,異步從 HTTP 服務器請求內容,其響應 (response) 可能以增量、隨機順序到達,也可能因網絡原因部分到達,而 WebKit 極大簡化了這些過程。WebKit 框架還簡化了顯示各種 MIME 類型內容過程,以及管理視圖中各元素滾動條。

WebKit 框架中的方法、函數只能在主線程或主隊列中調用。

1. WKWebView

WKWebView是 WebKit framework 的核心。在 app 內使用WKWebView插入網絡內容步驟如下:

  1. 創建WKWebView對象。
  2. WKWebView設置爲要顯示的視圖。
  3. WKWebView發送加載 Web 內容的請求。

使用initWithFrame:configuration:方法創建WKWebView,使用loadHTMLString:baseURL:加載本地HTML文件,或使用loadRequest:方法加載網絡內容。如下:

    // Local HTMLs
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.webConfiguration];
    self.view = webView;
    NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:@"WKWebView - NSHipster" withExtension:@"htm"];
    NSURL *baseURL = [htmlURL URLByDeletingLastPathComponent];
    NSString *htmlString = [NSString stringWithContentsOfURL:htmlURL
                                                    encoding:NSUTF8StringEncoding
                                                       error:NULL];
    [webView loadHTMLString:htmlString baseURL:baseURL];
    
    // Web Content
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.webConfiguration];
    self.view = webView;
    NSURL *myURL = [NSURL URLWithString:@"https://github.com/pro648/tips/wiki"];
    NSURLRequest *request = [NSURLRequest requestWithURL:myURL
                                             cachePolicy:NSURLRequestUseProtocolCachePolicy
                                         timeoutInterval:30];
    [webView loadRequest:request];

本地HTML文件可以在demo源碼中獲取:https://github.com/pro648/BasicDemos-iOS/tree/master/WebKit

如圖所示:

設置allowsBackForwardNavigationGestures屬性可以開啓、關閉橫向滑動觸發前進、後退導航功能:

self.webView.allowsBackForwardNavigationGestures = YES;

1.1 KVO

WKWebView中的titleURLestimatedProgresshasOnlySecureContentloading

屬性支持鍵值觀察,可以通過添加觀察者,獲得當前網頁標題、加載進度等。

根據文檔serverTrust屬性也支持KVO,但截至目前,在iOS 12.1 (16B91)中使用觀察者觀察該屬性,運行時會拋出this class is not key value coding-compliant for the key serverTrust的異常。

將網頁標題顯示出來可以幫助用戶瞭解當前所在位置,顯示當前導航進度能夠能夠讓用戶感受到加載速度,另外,還可以觀察hasOnlySecureContent查看當前網頁所有資源是否均通過加密連接傳輸。在viewDidLoad中添加以下代碼:

    [self.webView addObserver:self forKeyPath:@"hasOnlySecureContent" options:NSKeyValueObservingOptionNew context:webViewContext];
    [self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:webViewContext];
    [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:webViewContext];

實現observerValueForKeyPath:ofObject:change:context:方法,在觀察到值變化時進行對應操作:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"hasOnlySecureContent"]) {
        BOOL onlySecureContent = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
        NSLog(@"onlySecureContent:%@",onlySecureContent ? @"YES" : @"NO");
    } else if ([keyPath isEqualToString:@"title"]) {
        self.navigationItem.title = change[NSKeyValueChangeNewKey];
    } else if ([keyPath isEqualToString:@"estimatedProgress"]) {
        self.progressView.hidden = [change[NSKeyValueChangeNewKey] isEqualToNumber:@1];
        
        CGFloat progress = [change[NSKeyValueChangeNewKey] floatValue];
        self.progressView.progress = progress;
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

如果你對鍵值觀察、鍵值編碼還不熟悉,可以查看我的另一篇文章:KVC和KVO學習筆記

運行demo,如下所示:

控制檯會輸出如下內容:

onlySecureContent:YES

WKWebView調用reloadstopLoadinggoBackgoForward可以實現刷新、返回、前進等功能:

- (void)refreshButtonTapped:(id)sender {
    [self.webView reload];
}

- (void)stopLoadingButtonTapped:(id)sender {
    [self.webView stopLoading];
}

- (IBAction)backButtonTapped:(id)sender {
    [self.webView goBack];
}

- (IBAction)forwardButtonTapped:(id)sender {
    [self.webView goForward];
}

還可以通過觀察loading屬性,在視圖加載完成時,更新後退、前進按鈕狀態:

- (void)viewDidLoad {
    ...
    
    [self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:webViewContext];
}
    
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    ...
    if (context == webViewContext && [keyPath isEqualToString:@"loading"]) {
        BOOL loading = [change[NSKeyValueChangeNewKey] boolValue];
        // 加載完成後,右側爲刷新按鈕;加載過程中,右側爲暫停按鈕。
        self.navigationItem.rightBarButtonItem = loading ? self.stopLoadingButton : self.refreshButton;
        
        self.backButton.enabled = self.webView.canGoBack;
        self.forwardButton.enabled = self.webView.canGoForward;
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

1.2 截取網頁視圖

在iOS 11和macOS High Sierra中,WebKit framework增加了takeSnapshotWithConfiguration:completionHandler: API用於截取網頁視圖。截取網頁可見部分視圖方法如下:

- (IBAction)takeSnapShot:(UIBarButtonItem *)sender {
    WKSnapshotConfiguration *shotConfiguration = [[WKSnapshotConfiguration alloc] init];
    shotConfiguration.rect = CGRectMake(0, 0, self.webView.bounds.size.width, self.webView.bounds.size.height);
    
    [self.webView takeSnapshotWithConfiguration:shotConfiguration
                              completionHandler:^(UIImage * _Nullable snapshotImage, NSError * _Nullable error) {
                                  // 保存截圖至相冊,需要在info.plist中添加NSPhotoLibraryAddUsageDescription key和描述。
                                  UIImageWriteToSavedPhotosAlbum(snapshotImage, NULL, NULL, NULL);
                              }];
}

此前,截取網頁視圖需要結合圖層和graphics context。現在,只需要調用單一API。

1.3 執行JavaScript

可以使用evaluateJavaScript:completionHandler:方法觸發web view JavaScript。下面方法觸發輸出web view userAgent:

    [self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable userAgent, NSError * _Nullable error) {
        NSLog(@"%@",userAgent);
    }];

在iOS 12.1.2 (16C101) 中,輸出如下:

Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16C101

2. WKWebViewConfiguration

WKWebViewConfiguration是用於初始化Web視圖屬性的集合。通過WKWebViewConfiguration類,可以設置網頁渲染速度,視頻是否自動播放,HTML5 視頻是否一幀一幀播放,如何與本地代碼通信等。

WKWebViewConfiguration屬性有偏好設置preference、線程池processPool和用戶內容控制器userContentController等。

Web view 初始化時才需要WKWebViewConfiguration對象,WKWebView創建後無法修改其configuration。多個WKWebView可以使用同一個configuration。

例如,設置網頁中最小字體爲30,自動檢測電話號碼:

- (WKWebViewConfiguration *)webConfiguration {
    if (!_webConfiguration) {
        _webConfiguration = [[WKWebViewConfiguration alloc] init];
        
        // 偏好設置 設置最小字體
        WKPreferences *preferences = [[WKPreferences alloc] init];
        preferences.minimumFontSize = 30;
        _webConfiguration.preferences = preferences;
        
        // 識別網頁中的電話號碼
        _webConfiguration.dataDetectorTypes = WKDataDetectorTypePhoneNumber;
        
        // Web視圖內容完全加載到內存之前,禁止呈現。
        _webConfiguration.suppressesIncrementalRendering = YES;
    }
    return _webConfiguration;
}

suppressesIncrementalRendering屬性是布爾值,決定Web視圖內容在完全加載到內存前是否顯示,默認爲NO,即邊加載邊顯示。例如,Web視圖中有文字和圖片,會先顯示文字後顯示圖片。

3. Scripts

用戶腳本 (User Scripts) 是文檔開始加載或加載完成後注入 Web 頁面的 JS。User Scripts非常強大,其能夠通過客戶端設置網頁,允許注入事件監聽器,甚至可以注入腳本,這些腳本又可以回調 native app 。

3.1 WKUserScript

WKUserScript對象表示可以注入網頁的腳本。initWithSource:injectionTime:forMainFrameOnly:方法返回可以添加到userContentController控制器的腳本。其中,source 參數爲 script 源碼;injectionTime爲WKUserScriptInjectionTimeAtDocumentStartWKUserScriptInjectionTimeAtDocumentEnd

其參數如下:

  • source: script 源碼。
  • injectionTime: user script注入網頁時間,爲WKUserScriptInjectionTime枚舉常量。WKUserScriptInjectionTimeAtDocumentStart在創建文檔元素之後,加載任何其他內容之前注入。WKUserScriptInjectionTimeAtDocumentEnd在加載文檔後,但在加載其他子資源之前注入。
  • forMainFrameOnly: 布爾值,YES時只注入main frame,NO時注入所有 frame。

下面代碼將隱藏 Wikipedia toc、mw-panel 腳本注入網頁,同時使用 JS 提取網頁 toc 表格內容:

        // 隱藏wikipedia左邊緣和contents表格
        NSURL *hideTableOfContentsScriptURL = [[NSBundle mainBundle] URLForResource:@"hide" withExtension:@"js"];
        NSString *hideTableOfContentsScriptString = [NSString stringWithContentsOfURL:hideTableOfContentsScriptURL
                                                                             encoding:NSUTF8StringEncoding error:NULL];
        WKUserScript *hideTableOfContentsScript = [[WKUserScript alloc] initWithSource:hideTableOfContentsScriptString
                                                                         injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                                      forMainFrameOnly:YES];
        
        // 獲取contents表格內容
        NSString *fetchTableOfContentsScriptString = [NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"fetch" withExtension:@"js"] encoding:NSUTF8StringEncoding error:NULL];
        WKUserScript *fetchTableOfContentsScript = [[WKUserScript alloc] initWithSource:fetchTableOfContentsScriptString
                                                                          injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                                                                       forMainFrameOnly:YES];

本文中的 js 和 HTML 均可通過文章底部源碼鏈接獲取。要使用 JavaScript 提取內容的網頁爲:https://en.wikipedia.org/w/index.php?title=San_Francisco&mobileaction=toggle_view_desktop

3.2 WKUserContentController

WKUserContentController對象爲 JavaScript 提供了發送消息至 native app,將 user scripts 注入 Web 視圖方法。

將 user script 添加到userContentController纔可以注入網頁中:

        WKUserContentController *userContentController = [[WKUserContentController alloc] init];
        [userContentController addUserScript:hideTableOfContentsScript];
        [userContentController addUserScript:fetchTableOfContentsScript];

要監聽 JavaScript 消息,需要先註冊要監聽消息名稱。添加監聽事件方法爲addScriptMessageHandler:name:,參數如下:

  • scriptMessageHandler: 處理監聽消息,該類需要遵守WKMessageHandler協議。
  • name: 要監聽消息名稱。

使用該方法添加監聽事件後,JavaScript 的 window.webkit.messageHandlers.name.postMessage(messageBody) 函數將被定義在使用了該userContentController網頁視圖的所有frame。

監聽fetch.js中 didFetchTableOfContents 消息:

        [userContentController addScriptMessageHandler:self name:@"didFetchTableOfContents"];
        
        // 最後,將userContentController添加到WKWebViewConfiguration
        _webConfiguration.userContentController = userContentController;

3.3 WKScriptMessageHandler

監聽 script message 的類必須遵守WKMessageHandler協議,實現該協議唯一且必須實現的userContentController:didReceiveScriptMessage:方法。Webpage 接收到腳本消息時會調用該方法。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"didFetchTableOfContents"]) {
        id body = message.body;
        if ([body isKindOfClass:NSArray.class]) {
            NSLog(@"messageBody:%@",body);
        }
    }
}

如下所示:

JavaScript 消息是WKScriptMessage對象,該對象屬性如下:

  • body:消息內容,可以是NSNumberNSStringNSDateNSArrayNSDictionaryNSNull類型。
  • frameInfo:發送該消息的frame。
  • name:接收消息對象名稱。
  • webView:發送該消息的網頁視圖。

最終,我們成功的將事件從 iOS 轉發到 JavaScript,並將 JavaScript 轉發回 iOS。

4. WKNavigationDelegate

用戶點擊鏈接,使用前進、後退手勢,JavaScript 代碼(例如,window.location = ' https://github.com/pro648 '),使用代碼調用loadRequest:等均會讓網頁加載內容,即action引起網頁加載;隨後,web view 會向服務器發送request,接收response,可能會是positive response,也可能請求失敗;之後接收數據。我們的應用可以在action後、request前,或者response後、data前自定義網頁加載,決定繼續加載,或取消加載。

WKNavigationDelegate協議內方法可以自定義Web視圖接收、加載和完成導航請求過程的行爲。

首先,聲明遵守WKNavigationDelegate協議:

@interface ViewController () <WKNavigationDelegate>

其次,指定遵守WKNavigationDelegate協議的類爲 web view 代理:

self.webView.navigationDelegate = self;

最後,根據需要實現所需WKNavigationDelegate方法。

webView:decidePolicyForNavigationAction:decisionHandler:方法在action後響應,webView:decidePolicyForNavigationResponse:decisionHandler:方法在response後響應。

根據前面的配置,WKWebView會自動識別網頁中電話號碼。截至目前,電話號碼只能被識別,無法點擊。可以通過實現webView:decidePolicyForNavigationAction:decisionHandler:方法,調用系統Phone app撥打電話:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (navigationAction.navigationType == WKNavigationTypeLinkActivated && [navigationAction.request.URL.scheme isEqualToString:@"tel"]) {
        [UIApplication.sharedApplication openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) {
            NSLog(@"Successfully open url:%@",navigationAction.request.URL);
        }];
        decisionHandler(WKNavigationActionPolicyCancel);
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

實現了該方法後,必須調用decisionHandler塊。該塊參數爲WKNavigationAction枚舉常量。WKNavigationActionPolicyCancel取消導航,WKNavigationActionPolicyAllow繼續導航。

WKNavigationAction對象包含引起本次導航的信息,用於決定是否允許本次導航。其屬性如下:

  • request:本次導航的request。
  • sourceFrameWKFrameInfo類型,請求本次導航frame信息。
  • targetFrame:目標frame。如果導航至新窗口,則targetFramenil
  • navigationTypeWKNavigationType枚舉類型,爲以下常量:
    • WKNavigationTypeLinkActivated:用戶點擊href鏈接。
    • WKNavigationTypeFormSubmitted:提交表格。
    • WKNavigationTypeBackForward:請求前進、後退列表中item。
    • WKNavigationTypeReload:刷新網頁。
    • WKNavigationTypeFormResubmitted:因後退、前進、刷新等重新提交表格。
    • WKNavigationTypeOther:其他原因。

WKFrameInfo對象包含了一個網頁中的frame信息,其只是一個描述瞬時狀態 (transient) 的純數據 (data-only) 對象,不能在多次消息調用中唯一標誌某個frame。

如果需要在response後操作導航,需要實現webView:decidePolicyForNavigationResponse:decisionHandler:方法。WKNavigationResponse對象包含navigation response信息,用於決定是否接收response。其屬性如下:

  • canShowMIMEType:布爾類型值,指示WebKit是否顯示MIME類型內容。
  • forMainFrame:布爾類型值,指示即將導航至的frame是否爲main frame。
  • responseNSURLResponse類型。

實現了該方法後,必須調用decisionHandler塊,否則會在運行時拋出異常。decisionHandler塊參數爲WKNavigationResponsePolicy枚舉類型。WKNavigationResponseCancel取消導航,WKNavigationResponseAllow繼續導航。

Navigation action 和 navigation response 既可以在處理完畢後立即調用decisionHandler,也可以異步調用。

5. WKUIDelegate

WKWebView與 Safari 類似,儘管前者在一個窗口顯示內容。如果需要打開多個窗口、監控打開、關閉窗口,修改用戶點擊元素時顯示哪些選項,需要使用WKUIDelegate協議。

首先,聲明遵守WKUIDelegate協議:

@interface ViewController () <WKUIDelegate>

其次,指定遵守WKUIDelegate協議的類爲 web view 代理:

self.webView.uiDelegate = self;

最後,根據需要實現WKUIDelegate協議內方法。

5.1 新窗口打開

如何響應 JavaScript 的打開新窗口函數、或target="_blank"標籤?有以下三種方法:

  1. 創建新的WKWebView,並在新的頁面打開。
  2. 在 Safari 瀏覽器打開。
  3. 捕捉 JS ,在同一個WKWebView加載。

當 URL 爲 mail、tel、sms 和 iTunes鏈接時交由系統處理。此時,系統會交由對應 app 處理。其他情況在當前 web view 加載。

- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    if (navigationAction.targetFrame == nil) {
        NSURL *url = navigationAction.request.URL;
        NSString *itunes = @"^https?:\\/\\/itunes\\.apple\\.com\\/\\S+";
        NSString *mail = @"^mailto:\\S+";
        NSString *tel = @"^tel:\\/\\/\\+?\\d+";
        NSString *sms = @"^sms:\\/\\/\\+?\\d+";
        NSString *pattern = [NSString stringWithFormat:@"(%@)|(%@)|(%@)|(%@)",itunes, mail, tel, sms];
        if ([self validatePath:url.absoluteString withPattern:pattern]) {
            [UIApplication.sharedApplication openURL:url
                                             options:@{}
                                   completionHandler:nil];
        } else {
            [webView loadRequest:navigationAction.request];
        }
    }
    return nil;
}

- (BOOL)validatePath:(NSString *)path withPattern:(NSString *)pattern {
    NSError *error = nil;
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
                                                                           options:NSRegularExpressionCaseInsensitive
                                                                             error:&error];
    if (error) {
        return NO;
    }
    
    NSRange range = [regex rangeOfFirstMatchInString:path
                                             options:NSMatchingReportCompletion
                                               range:NSMakeRange(0, path.length)];
    
    if (range.location == NSNotFound) {
        return NO;
    } else {
        return YES;
    }
}

5.2 響應 JavaScript 彈窗

在響應 JavaScript 時,可以通過WKUIDelegate協議使用 native UI呈現,有以下三種方法:

  • webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler::爲了用戶安全,在該方法的實現中需要需要標誌出提供當前內容的網址,最爲簡便的方法便是frame.request.URL.host,響應面板應只包含一個OK按鈕。當 alert panel 消失後,調用completionHandler
  • webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler::爲了用戶安全,在該方法的實現中需要需要標誌出提供當前內容的網址,最爲簡便的方法便是frame.request.URL.host,響應面板包括兩個按鈕,一般爲OKCancel。當 alert panel 消失後,調用completionHandler。如果用戶點擊的是OK按鈕,爲completionHandler傳入YES;如果用戶點擊的是Cancel按鈕,爲completionHandler傳入NO
  • webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler::爲了用戶安全,在該方法的實現中需要需要標誌出提供當前內容的網址,最爲簡便的方法便是frame.request.URL.host,響應面板包括兩個按鈕(一個OK按鈕,一個Cancel按鈕)和一個輸入框。當面板消失時調用completionHandler,如果用戶點擊的是OK按鈕,傳入文本框文本;否則,傳入nil

例如,輸入賬號、密碼前點擊登陸按鈕,大部分網頁會彈出警告框。在 JavaScript 中,會彈出 alert 或 confirm box。

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    NSString *msg = [NSString stringWithFormat:@"%@ %@",message, frame.request.URL.absoluteString];
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert Panel"
                                                                             message:msg
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK"
                                                       style:UIAlertActionStyleDefault
                                                     handler:^(UIAlertAction * _Nonnull action) {
                                                         completionHandler();
                                                     }];
    [alertController addAction:okAction];
    [self presentAlertController:alertController];
}

- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
    NSString *msg = [NSString stringWithFormat:@"%@ %@",frame.request.URL.absoluteString, message];
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Confirm Panel"
                                                                             message:msg
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"
                                                           style:UIAlertActionStyleCancel
                                                         handler:^(UIAlertAction * _Nonnull action) {
                                                             completionHandler(NO);
                                                         }];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK"
                                                       style:UIAlertActionStyleDefault
                                                     handler:^(UIAlertAction * _Nonnull action) {
                                                         completionHandler(YES);
                                                     }];
    [alertController addAction:cancelAction];
    [alertController addAction:okAction];
    [self presentAlertController:alertController];
}

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Text Input Panel"
                                                                             message:prompt
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"
                                                           style:UIAlertActionStyleCancel
                                                         handler:^(UIAlertAction * _Nonnull action) {
                                                             completionHandler(nil);
                                                         }];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK"
                                                       style:UIAlertActionStyleDefault
                                                     handler:^(UIAlertAction * _Nonnull action) {
                                                         UITextField *textField = alertController.textFields.firstObject;
                                                         completionHandler(textField.text);
                                                     }];
    [alertController addAction:cancelAction];
    [alertController addAction:okAction];
    [self presentAlertController:alertController];
}

- (void)presentAlertController:(UIAlertController *)alertController {
    UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
    [vc presentViewController:alertController
                     animated:YES
                   completion:nil];
}

JavaScript alert 會堵塞當前進程,調用completionHandler後 JavaScript 纔會繼續執行。

6. WKURLSchemeHandler

UIWebView支持自定義NSURLProtocol協議。如果想要加載自定義 URL 內容,可以通過創建、註冊NSURLProtocol子類實現。此後,任何調用自定義 scheme (例如,hello world://) 的方法,都會調用NSURLProtocol子類,在NSURLProtocol子類處理自定義 scheme ,這將非常實用。例如,在 book 中加載圖片、視頻等。

WKWebView不支持NSURLProtocol協議,因此不能加載自定義 URL Scheme。在 iOS 11 中,Apple 爲 WebKit framework 增加了WKURLSchemeHandler協議,用於加載自定義 URL Scheme。

WebKit遇到無法識別的 URL時,會調用WKURLSchemeHandler協議。該協議包括以下兩個必須實現的方法:

  • webView:startURLSchemeTask::加載資源時調用。
  • webView:stopURLSchemeTask::WebKit 調用該方法以終止 (stop) 任務。調用該方法後,不得調用WKURLSchemeTask協議的任何方法,否則會拋出異常。

使用WKURLSchemeHandler協議處理完畢任務後,調用WKURLSchemeTask協議內方法加載資源。WKURLSchemeTask協議包括request屬性,該屬性爲NSURLRequest類型對象。還包含以下方法:

  1. didReceiveResponse::設置當前任務的 response。每個 task 至少調用一次該方法。如果嘗試在任務終止或完成後調用該方法,則會拋出異常。
  2. didReceiveData::設置接收到的數據。當接收到任務最後的 response 後,使用該方法發送數據。每次調用該方法時,新數據會拼接到先前收到的數據中。如果嘗試在發送 response 前,或任務完成、終止後調用該方法,則會引發異常。
  3. didFinish:將任務標記爲成功完成。如果嘗試在發送 response 前,或將已完成、終止的任務標記爲完成,則會引發異常。
  4. didFailWithError::將任務標記爲失敗。如果嘗試將已完成、失敗,終止的任務標記爲失敗,則會引發異常。

WKURLSchemeHandler協議方法內,可以獲取到請求的request。因此,可以提取 URL 中任何內容,並將數據轉發給WKWebView進行加載。

下面的方法分別使用 url 、custom URL Scheme加載網絡圖片和相冊圖片:

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        NSURL *url = urlSchemeTask.request.URL;
        if ([url.absoluteString containsString:@"custom-scheme"]) {
            NSArray<NSURLQueryItem *> *queryItems = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES].queryItems;
            for (NSURLQueryItem *item in queryItems) {
                
                // example: custom-scheme://path?type=remote&url=https://placehold.it/120x120&text=image1
                if ([item.name isEqualToString:@"type"] && [item.value isEqualToString:@"remote"]) {
                    for (NSURLQueryItem *queryParams in queryItems) {
                        if ([queryParams.name isEqualToString:@"url"]) {
                            NSString *path = queryParams.value;
                            path = [path stringByReplacingOccurrencesOfString:@"\\" withString:@""];
                            
                            // 獲取圖片
                            NSURLSession *session = [NSURLSession sharedSession];
                            NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:path] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                [urlSchemeTask didReceiveResponse:response];
                                [urlSchemeTask didReceiveData:data];
                                [urlSchemeTask didFinish];
                            }];
                            [task resume];
                        }
                    }
                } else if ([item.name isEqualToString:@"type"] && [item.value isEqualToString:@"photos"]) { // example: custom-scheme://path?type=photos
                    dispatch_async(dispatch_get_main_queue(), ^{
                        self.imagePicker = [[ImagePicker alloc] init];
                        [self.imagePicker showGallery:^(BOOL flag, NSURLResponse * _Nonnull response, NSData * _Nonnull data) {
                            if (flag) {
                                [urlSchemeTask didReceiveResponse:response];
                                [urlSchemeTask didReceiveData:data];
                                [urlSchemeTask didFinish];
                            } else {
                                NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
                                [urlSchemeTask didFailWithError:error];
                            }
                        }];
                    });
                }
            }
        }
    });
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
    [urlSchemeTask didFailWithError:error];
}

實現上述方法的類必須遵守WKURLSchemeHandler協議。另外,必須在WKWebView的配置中註冊所支持的 URL Scheme:

        // 添加要自定義的url scheme
        [_webConfiguration setURLSchemeHandler:self forURLScheme:@"custom-scheme"];

運行如下:

其中,image1 通過普通 URL 加載,120*120 和相冊圖片使用自定義 URL Scheme 加載。

總結

WebKit爲 iOS 、macOS 開發人員提供了一套強大的開發工具,可以直接在 app 網頁視圖中操作 JavaScript,使用 user script 將 JavaScript 注入網頁,使用WKScriptMessageHandler協議接收 JavaScript 消息。使用WKNavigationDelegate協議自定義網頁導航,使用WKUIDelegate在網頁上呈現 native UI,使用WKURLSchemeHandler加載自定義 URL Scheme 內容。

如果只是簡單呈現網頁視圖,推薦使用 iOS 9 推出的SFSafariViewController,幾行代碼就可實現與 Safari 一樣的體驗。SFSafariViewController還提供了自動填充、欺詐網站監測等功能。

Demo名稱:WebKit
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/WebKit

參考資料:

  1. Introducing the Modern WebKit API
  2. A Look at the WebKit Framework – Part 1
  3. Tips For WKWebView That Will Keep You From Going Troubles
  4. Why is WKWebView not opening links with target=“_blank”?
  5. Tips For WKWebView That Will Keep You From Going Troubles
  6. JavaScript Manipulation on iOS Using WebKit

歡迎更多指正:https://github.com/pro648/tips/wiki

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章