Web view 用於加載和顯示豐富的網絡內容。例如,嵌入 HTML 和網站。Mail app 使用 web view 顯示郵件中的 HTML 內容。
iOS 8 和 macOS 10.10 中引入了WebKit framework,用以取代UIWebView
和WebView
。同時,在兩個平臺上提供同一API。與UIWebView
相比,WebKit 有以下優勢:使用了與 Safari 一樣的 JavaScript engine,在運行腳本前,將腳本編譯爲機器代碼,速度更快;支持多進程架構,Web 內容在單獨的線程中運行,WKWebView
崩潰不會影響 app運行;能夠以60fps滑動。另外,在 iOS 12 和 macOS 10.14中,UIWebView
和WebView
已經被正式棄用。
WebKit framework 提供了多個類和協議,用於在窗口中顯示網絡內容,並實現類似瀏覽器功能。例如,點擊鏈接時顯示鏈接內容,維護前進、後退列表,維護最近訪問列表。加載網頁內容時,異步從 HTTP 服務器請求內容,其響應 (response) 可能以增量、隨機順序到達,也可能因網絡原因部分到達,而 WebKit 極大簡化了這些過程。WebKit 框架還簡化了顯示各種 MIME 類型內容過程,以及管理視圖中各元素滾動條。
WebKit 框架中的方法、函數只能在主線程或主隊列中調用。
1. WKWebView
WKWebView
是 WebKit framework 的核心。在 app 內使用WKWebView
插入網絡內容步驟如下:
- 創建
WKWebView
對象。 - 將
WKWebView
設置爲要顯示的視圖。 - 向
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
中的title
、URL
、estimatedProgress
、hasOnlySecureContent
和loading
屬性支持鍵值觀察,可以通過添加觀察者,獲得當前網頁標題、加載進度等。
根據文檔
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
調用reload
、stopLoading
、goBack
、goForward
可以實現刷新、返回、前進等功能:
- (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爲WKUserScriptInjectionTimeAtDocumentStart
、WKUserScriptInjectionTimeAtDocumentEnd
,
其參數如下:
-
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
:消息內容,可以是NSNumber
、NSString
、NSDate
、NSArray
、NSDictionary
、NSNull
類型。 -
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。 -
sourceFrame
:WKFrameInfo
類型,請求本次導航frame信息。 -
targetFrame
:目標frame。如果導航至新窗口,則targetFrame
爲nil
。 -
navigationType
:WKNavigationType
枚舉類型,爲以下常量:-
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。 -
response
:NSURLResponse
類型。
實現了該方法後,必須調用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"
標籤?有以下三種方法:
- 創建新的
WKWebView
,並在新的頁面打開。 - 在 Safari 瀏覽器打開。
- 捕捉 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
,響應面板包括兩個按鈕,一般爲OK
和Cancel
。當 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
類型對象。還包含以下方法:
-
didReceiveResponse:
:設置當前任務的 response。每個 task 至少調用一次該方法。如果嘗試在任務終止或完成後調用該方法,則會拋出異常。 -
didReceiveData:
:設置接收到的數據。當接收到任務最後的 response 後,使用該方法發送數據。每次調用該方法時,新數據會拼接到先前收到的數據中。如果嘗試在發送 response 前,或任務完成、終止後調用該方法,則會引發異常。 -
didFinish
:將任務標記爲成功完成。如果嘗試在發送 response 前,或將已完成、終止的任務標記爲完成,則會引發異常。 -
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
參考資料: