WKWebView 的使用和封裝

WKWebView 的使用和封裝

前言

項目中有個新聞資訊模塊展現公司和相關行業的最新動態。
這個部分基本是以展現網頁爲主,內部可能會有一些 native 和 JS 代碼的交互。
由於是新項目,因此決定採用 iOS 8 中新出的 WebKit。
本文是對 WebKit 框架中 WKWebView 的一些學習和封裝css

WKWebViewDemo 地址html

UIWebView 和 WKWebView

這二者都是 iOS 中展現 web 相關的組件。前者 iOS 2.0 就有了,後者是 iOS 8.0 時候新加的。java

網絡中關於二者的差別和性能對比分析不少,這裏再也不贅述。只是說明一下蘋果文檔中的重要提示以及本身須要功能的實現:ios

官方文檔中重要提示
Snip20180629_1.pngnginx

文檔中主要說了如下幾點:git

  • iOS 8 以後,應該用 WKWebView 代替 UIWebView。並能夠設置 WKPreferencesjavaScriptEnabled 屬性決定是否支持 web 內容執行 JavaScript 代碼
  • iOS 10 以後必須在 info.plist 文件中包含要訪問數據的描述。對於圖庫的訪問必須包含 NSPhotoLibraryUsageDescriptionNSCameraUsageDescription 不然會直接 crash
  • 加載本地 HTML 文件使用: loadHTMLString:baseURL: 方法
  • 加載網絡內容使用 : loadRequest: 方法
  • stopLoading 方法用來結束加載。loading 屬性查看WK進程中是否加載中
  • goBackgoForward 方法可用於 WKWebView 的前進後退. canGoBackcanGoForward 屬性來查看是否能前進後退
  • 一般WKWebView會自動識別電話號碼,並把它設置成可打電話的連接。若是不用這個功能: 設置 dataDetectorTypes 屬性中 UIDataDetectorTypes 的位字段不包含 UIDataDetectorTypePhoneNumber.
  • scalesPageToFit 屬性用於第一次加載網頁內容時候設置是否可使用手勢改變網頁縮放。 設置後用戶就能夠手勢縮放網頁大小
  • 記錄網頁加載網絡內容能夠設置WKWebView 的 delegate 並遵照 UIWebViewDelegate 協議
  • 不要在 網頁中嵌入 UIScrollView 及其子類,這樣會致使手勢等行爲混亂

有了基本的概念,就能夠去看一下 WKWebView 的具體文檔了。若是怕官方文檔麻煩也能夠直接看網絡上別人整理好的網絡整理github

下面是我整理的 WebView 和 H5 調用邏輯圖:web

Snip20180703_2.png

特別說明一點:json

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

功能需求

對於項目而言,網頁功能無需太多,主要知足如下幾點canvas

基本展現功能

  1. 導航欄下顯示加載進度條
  2. 導航欄 title 展現網頁內容當前 title
  3. 網頁內容的刷新、前進、後退
  4. 網頁內容加載、刷新過程當中 HUD 提示

JS和OC交互功能:交互的數據格式和方法名等須要和H5端具體協調

  1. App內登陸後,訪問 web 須要對應用戶token
  2. 網頁中點擊超連接,新開頁面處理,同上也須要攔截新URL請求並補全token參數
  3. 跟JS交互中對JS返回值的處理
  4. 簡單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 的小封裝能實現目前所需功能,但不少內容還須要在須要的時候去探究,但願能幫到一樣學習的小夥伴。

若是看完有收穫,不妨點個贊,讓我也更有分享的動力!

相關文章
相關標籤/搜索