WKWebView 的使用和封裝
前言
項目中有個新聞資訊模塊展示公司和相關行業的最新動態。
這個部分基本是以展示網頁為主,內部可能會有一些 native 和 JS 代碼的交互。
因為是新項目,所以決定采用 iOS 8 中新出的 WebKit。
本文是對 WebKit 框架中 WKWebView 的一些學習和封裝
UIWebView 和 WKWebView
這兩者都是 iOS 中展示 web 相關的組件。前者 iOS 2.0 就有了,后者是 iOS 8.0 時候新加的。
網絡中關於兩者的差異和性能對比分析很多,這里不再贅述。只是說明一下蘋果文檔中的重要提示以及自己需要功能的實現:
官方文檔中重要提示
文檔中主要說了以下幾點:
- iOS 8 之后,應該用 WKWebView 代替 UIWebView。並可以設置
WKPreferences
的javaScriptEnabled
屬性決定是否支持 web 內容執行 JavaScript 代碼 - iOS 10 之后必須在 info.plist 文件中包含要訪問數據的描述。對於圖庫的訪問必須包含
NSPhotoLibraryUsageDescription
和NSCameraUsageDescription
否則會直接 crash - 加載本地 HTML 文件使用:
loadHTMLString:baseURL:
方法 - 加載網絡內容使用 :
loadRequest:
方法 -
stopLoading
方法用來結束加載。loading
屬性查看WK進程中是否加載中 goBack
和goForward
方法可用於 WKWebView 的前進后退.canGoBack
和canGoForward
屬性來查看是否能前進后退- 通常WKWebView會自動識別電話號碼,並把它設置成可打電話的鏈接。如果不用這個功能: 設置
dataDetectorTypes
屬性中UIDataDetectorTypes
的位字段不包含UIDataDetectorTypePhoneNumber
. scalesPageToFit
屬性用於第一次加載網頁內容時候設置是否可以使用手勢改變網頁縮放。 設置后用戶就可以手勢縮放網頁大小- 記錄網頁加載網絡內容可以設置WKWebView 的
delegate
並遵守UIWebViewDelegate
協議 - 不要在 網頁中嵌入 UIScrollView 及其子類,這樣會導致手勢等行為混亂
有了基本的概念,就可以去看一下 WKWebView 的具體文檔了。如果怕官方文檔麻煩也可以直接看網絡上別人整理好的網絡整理。
下面是我整理的 WebView 和 H5 調用邏輯圖:
特別說明一點:
0. OC 執行 JS 方法
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler;
這個方法中webView調用JS,block只是成功或失敗的回調
1. JS方法中
window.webkit.messageHandlers.webViewApp.postMessage(message);
作用是JS 向之前注冊的 webViewApp 發送消息。
OC 端接到消息會調用 <WKScriptMessageHandler> 中下面代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
功能需求
對於項目而言,網頁功能無需太多,主要滿足以下幾點
基本展示功能
- 導航欄下顯示加載進度條
- 導航欄 title 展示網頁內容當前 title
- 網頁內容的刷新、前進、后退
- 網頁內容加載、刷新過程中 HUD 提示
JS和OC交互功能:交互的數據格式和方法名等需要和H5端具體協調
- App內登錄后,訪問 web 需要對應用戶token
- 網頁中點擊超鏈接,新開頁面處理,同上也需要攔截新URL請求並補全token參數
- 跟JS交互中對JS返回值的處理
- 簡單JS代碼注入,如資訊內容底部加一些贊和分享等功能<曾經就有接口只返回一段JS>
以上這些基本功能中基本展示功能都比較簡單,和JS交互的部分需要和 H5 端小伙伴共同定義數據結構和互調的方法名、參數等。所以需要具體問題具體分析。項目以我自己 Demo 為例說一下。
功能實現
為實現功能主要封裝了三個類
XYWKWebViewController ---> 管理 webView 加載相關的代理方法
XYWKWebView ---> 封裝 webView 請求相關方法
XYScriptMessage ---> 封裝JS回調信息
XYWKWebView 核心功能
加載本地HTML文件
/**
* 加載本地HTML頁面
*
* @param htmlName html頁面文件名稱
*/
- (void)loadLocalHTMLWithFileName:(nonnull NSString *)htmlName
實現代碼
- (void)loadLocalHTMLWithFileName:(nonnull NSString *)htmlName {
NSString *path = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithPath:path];
NSString * htmlPath = [[NSBundle mainBundle] pathForResource:htmlName
ofType:@"html"];
NSString * htmlCont = [NSString stringWithContentsOfFile:htmlPath
encoding:NSUTF8StringEncoding
error:nil];
// WKWebView 的 loadHTMLString: 方法
[self loadHTMLString:htmlCont baseURL:baseURL];
}
加載網絡內容
// 加載網絡內容,根據是否有參數選不同方法
- (void)loadRequestWithRelativeUrl:(nonnull NSString *)relativeUrl;
- (void)loadRequestWithRelativeUrl:(nonnull NSString *)relativeUrl params:(nullable NSDictionary *)params;
實現代碼
- (void)loadRequestWithRelativeUrl:(NSString *)relativeUrl params:(NSDictionary *)params {
NSURL *url = [self generateURL:relativeUrl params:params];
[self loadRequest:[NSURLRequest requestWithURL:url]];
}
- (NSURL *)generateURL:(NSString*)baseURL params:(NSDictionary*)params {
self.webViewRequestUrl = baseURL;
self.webViewRequestParams = params;
NSMutableDictionary *param = [NSMutableDictionary dictionaryWithDictionary:params];
NSMutableArray* pairs = [NSMutableArray array];
//可以在這里將token參數添加進去,這樣就可以實現補全token功能
for (NSString* key in param.keyEnumerator) {
NSString *value = [NSString stringWithFormat:@"%@",[param objectForKey:key]];
NSString* escaped_value = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
(__bridge CFStringRef)value,
NULL,
(CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ",
kCFStringEncodingUTF8);
[pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]];
}
NSString *query = [pairs componentsJoinedByString:@"&"];
baseURL = [baseURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString* url = @"";
if ([baseURL containsString:@"?"]) {
url = [NSString stringWithFormat:@"%@&%@",baseURL, query];
}
else {
url = [NSString stringWithFormat:@"%@?%@",baseURL, query];
}
//絕對地址
if ([url.lowercaseString hasPrefix:@"http"]) {
return [NSURL URLWithString:url];
}
else {
return [NSURL URLWithString:url relativeToURL:self.baseUrl];
}
}
XYWKWebViewController 核心功能
這是一個 Controller,建議創建新的Controller繼承XYWKWebViewController 來使用,這樣可以把不同的頁面區分開,每個頁面加載的url和相關的業務邏輯都可以單獨處理,代碼易讀,也容易維護。如果項目后期添加功能也好處理
XYWKWebViewController主要完成了對一些功能的封裝,比如進度條、頁面title以及 webView 的生命周期。
進度條和title都是通過KVO實現
if (self.shouldShowProgress) {
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
}
if (self.isUseWebPageTitle) {
[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
}
設置title 和 progressView 直接是自己簡單寫了一個 View
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"estimatedProgress"]) {
if (object == self.webView) {
[self showLoadingProgress:self.webView.estimatedProgress andTintColor:[UIColor colorWithRed:24/255.0 green:124/255.0 blue:244/255.0f alpha:1.0]];
}
else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
else if ([keyPath isEqualToString:@"title"]){
if (object == self.webView) {
if ([self isUseWebPageTitle]) {
self.title = self.webView.title;
}
}
else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
OC 與 JS 之間交互的處理
這部分是可定制化功能最多的,遇到的問題也是最多的。WKWebView 和 JS 之間的交互需要設置 ScriptMessageHandler 如下。
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration {
self = [super initWithFrame:frame configuration:configuration];
if (self) {
if (configuration) {
//文檔中說
//Adds a script message handler.
//Adding a script message handler with name name causes the JavaScript function window.webkit.messageHandlers.name.postMessage(messageBody) to be defined in all frames in all web views that use the user content controller.
// 這里就是設置 網頁中 JS Message handler
// 通過 “name” 注冊之后,JS 內部函數 window.webkit.messageHandlers.“name”.postMessage(messageBody) 就被定義到整個用戶的Web內容的控制器中。
//后面的JS調用OC也是通過 “name” 聯系的
[configuration.userContentController addScriptMessageHandler:self name:@"webViewApp"];
}
//默認允許系統自帶的側滑后退
self.allowsBackForwardNavigationGestures = YES;
}
return self;
}
然后實現 WKScriptMessageHandler 代理
// JS 調用 OC 的回調。JS 向OC 發送消息會調用這個方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"得到的 JS message 是 :%@",message.body);
if ([message.body isKindOfClass:[NSDictionary class]]) {
NSDictionary *body = (NSDictionary *)message.body;
// 這里是對 JS 消息的一個處理,用自己定義的消息類型,封裝並發送給代理去外部處理,具體格式需要工作中和H5共同制定
XYScriptMessage *msg = [XYScriptMessage new];
[msg setValuesForKeysWithDictionary:body];
if (self.xy_messageHandlerDelegate && [self.xy_messageHandlerDelegate respondsToSelector:@selector(xy_webView:didReceiveScriptMessage:)]) {
[self.xy_messageHandlerDelegate xy_webView:self didReceiveScriptMessage:msg];
}
}
}
其中自定義的 XYScriptMessage 如下
/**
* WKWebView與JS調用時參數規范實體
*/
@interface XYScriptMessage : NSObject
/**
* 方法名
* 用來確定Native App的執行邏輯
*/
@property (nonatomic, copy) NSString *method;
/**
* 方法參數
* json字符串
*/
@property (nonatomic, copy) NSDictionary *params;
/**
* 回調函數名
* Native App執行完后回調的JS方法名
*/
@property (nonatomic, copy) NSString *callback;
@end
同時提供delegate方法供XYWKWebViewController實現
/**
* JS調用原生方法處理,其中方法名都需要和 H5 端相互協調
*/
- (void)xy_webView:(XYWKWebView *)webView didReceiveScriptMessage:(XYScriptMessage *)message {
NSLog(@"webView method:%@",message.method);
//返回上一頁
if ([message.method isEqualToString:@"tobackpage"]) {
[self.navigationController popViewControllerAnimated:YES];
}
//打開新頁面
else if ([message.method isEqualToString:@"openappurl"]) {
NSString *url = [message.params objectForKey:@"url"];
if (url.length) {
XYWKWebViewController *webViewController = [[XYWKWebViewController alloc] init];
webViewController.url = url;
[self.navigationController pushViewController:webViewController animated:YES];
}
}
}
使用方法
一個提供四類使用功能,使用方法建議直接繼承 XYWKWebViewController。
class WebViewController: XYWKWebViewController {
override func viewDidLoad() {
super.viewDidLoad()
/// #用法0: 直接加載對應的地址 <沒有參數>
//self.webView.loadRequest(withRelativeUrl: "https://www.httpbin.org/")
/// #用法1: 直接加載對應的地址 <有參數>
//let params = ["name":"xiaoyou",
// "password" : "123456#/HTTP_Methods/get_get"]
//self.webView.loadRequest(withRelativeUrl: "https://www.httpbin.org/", params: params)
/// #用法2: 直接加載本地HTML文件 <沒有參數>
self.webView.loadLocalHTML(withFileName: "main")
/// #用法3: JS 注入,添加一些方法 <這里的原生坐標和JS之間無法直接相對應>
let margin : CGFloat = 6.0
let padding : CGFloat = 10.0
let width = UIScreen.main.bounds.size.width - (margin * 2.0) - (margin * 7.0 + padding)
let btnWidth = (width - padding - 5) / 2.0
let styleJS = """
<style type="text/css">
#foot {
border:solid 10px #600;
padding:\(padding)px;
margin:\(margin)px;
clear:both;
width:\(width)px
}
#share {
border:solid 1px #600;
padding:2px;
margin:2px;
clear:both;
width:\(btnWidth)px;
heiht:150px
}
#like {
border:solid 1px #600;
padding:2px;
margin:2px;
clear:both;
width:\(btnWidth)px;
heiht:50px
}
</style>
"""
let funcJS = """
\t\t\tfunction testFunc(text){\n
\t\t\t\tvar message = \"點我干什么\";\n
\t\t\t\twindow.webkit.messageHandlers.webViewApp.postMessage(message);\n
\t\t\t\talert(text);\n
\t\t\t}\n
"""
let footerJS = """
\t<button onclick=\"testFunc('http://www.baidu.com/')\">自己添加的Footer的Button一個</button><br /><br /><br />\n
\t <div id=\"foot\">底部說明 <br />
<button id=\"share\" onclick=\"testFunc('分享')\">分享</button>
<button id=\"like\" onclick=\"testFunc('點贊')\">點贊</button><br />
</div>
"""
self.webView.loadLocalHTML("main", withAddingStyleJS: styleJS, funcJS: funcJS, footerJS: footerJS)
/// 設置導航
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "返回", style: .plain, target: self, action: #selector(backAction));
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "調用JS", style: .plain, target: self, action: #selector(callJS));
}
}
/// #用法4: OC 調用JS方法。這里可以調用JS,把H5需要的參數傳給他們
/// 這里是JS 回調方法
extension WebViewController{
@objc func backAction() {
self.dismiss(animated: true, completion: nil)
}
@objc func callJS() {
self.webView.callJS("call('Hello World!')") { (response) in
print("\(String(describing: response))")
}
}
/// 這里是重寫了WebView接受到JS消息的回調,需要調用super方法才能執行內部方法,否則這里只是打印
override func xy_webView(_ webView: XYWKWebView, didReceive message: XYScriptMessage) {
// 如果完全自定義的js方法處理,無需重寫父類,自行實現即可
super.xy_webView(webView, didReceive: message)
print(message)
}
}
具體見Demo
遇到的問題
HTML 中超鏈接,需要打開新頁面的"_blank"處理
小結
WebKit 的小封裝能實現目前所需功能,但很多內容還需要在需要的時候去探究,希望能幫到同樣學習的小伙伴。
如果看完有收獲,不妨點個贊,讓我也更有分享的動力!