WKWebView新特性及JS交互

目錄(?)[+]

聲明

本博文原始地址在: 
http://mp.weixin.qq.com/s?__biz=MzIzMzA4NjA5Mw==&mid=400327803&idx=1&sn=2a09fa94dd605a9f03bbc16f998e5717#rd

本博客不會在此處更新文章,請關注微信公衆號,更新的內容只會是在原文更新。

引言

一直聽說WKWebViewUIWebView強大許多,可是一直沒有使用到,今天花了點時間看寫了個例子,對其API的使用有所瞭解,爲了日後能少走彎路,也爲了讓大家更容易學習上手,這裏寫下這篇文章來記錄如何使用以及需要注意的地方。

溫馨提示:本人在學習使用過程中,確實有此體會,WKWebView的確比UIWebView強大很多,與js交互的能力顯示增強,在加載速度上有所提升。

WKWebView新特性

  • 性能、穩定性、功能大幅度提升
  • 允許JavaScript的Nitro庫加載並使用(UIWebView中限制)
  • 支持了更多的HTML5特性
  • 高達60fps的滾動刷新率以及內置手勢
  • GPU硬件加速
  • KVO
  • 重構UIWebView成14類與3個協議,查看官方文檔

準備工作

首先,我們在使用的地方引入模塊:

import Webkit
  • 1
  • 1

在學習之前,建議大家先點擊WKWebView進去閱讀裏面的相關API,讀完一遍,有個大概的印象,學習起來就很快了。

其初始化方法有:

public init()
public init(frame: CGRect)
public init(frame: CGRect, configuration: WKWebViewConfiguration)
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

加載HTML的方式與UIWebView的方式大致相同。其中,loadFileURL方法通常用於加載服務器的HTML頁面或者JS,而loadHTMLString方法通常用於加載本地HTML或者JS:

public func loadRequest(request: NSURLRequest) -> WKNavigation?

// 9.0以後才支持   
@available(iOS 9.0, *)
public func loadFileURL(URL: NSURL, allowingReadAccessToURL readAccessURL: NSURL) -> WKNavigation?

// 通常用於加載本地HTML或者JS
public func loadHTMLString(string: String, baseURL: NSURL?) -> WKNavigation?

// 9.0以後才支持
@available(iOS 9.0, *)
public func loadData(data: NSData, MIMEType: String, characterEncodingName: String, baseURL: NSURL) -> WKNavigation
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

與之交互用到的三大代理:

  • WKNavigationDelegate,與頁面導航加載相關
  • WKUIDelegate,與JS交互時的ui展示相關,比較JS的alert、confirm、prompt
  • WKScriptMessageHandler,與js交互相關,通常是ios端注入名稱,js端通過window.webkit.messageHandlers.{NAME}.postMessage()來發消息到ios端

創建WKWebView

首先,我們在ViewController中先遵守協議:

class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate
  • 1
  • 1

我們可以先創建一個WKWebView的配置項WKWebViewConfiguration,這可以設置偏好設置,與網頁交互的配置,注入對象,注入js等:

// 創建一個webiview的配置項
let configuretion = WKWebViewConfiguration()

// Webview的偏好設置
configuretion.preferences = WKPreferences()
configuretion.preferences.minimumFontSize = 10
configuretion.preferences.javaScriptEnabled = true
// 默認是不能通過JS自動打開窗口的,必須通過用戶交互才能打開
configuretion.preferences.javaScriptCanOpenWindowsAutomatically = false

// 通過js與webview內容交互配置
configuretion.userContentController = WKUserContentController()

// 添加一個JS到HTML中,這樣就可以直接在JS中調用我們添加的JS方法
let script = WKUserScript(source: "function showAlert() { alert('在載入webview時通過Swift注入的JS方法'); }",
  injectionTime: .AtDocumentStart,// 在載入時就添加JS
  forMainFrameOnly: true) // 只添加到mainFrame中
configuretion.userContentController.addUserScript(script)

// 添加一個名稱,就可以在JS通過這個名稱發送消息:
// window.webkit.messageHandlers.AppModel.postMessage({body: 'xxx'})
configuretion.userContentController.addScriptMessageHandler(self, name: "AppModel")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

創建對象並遵守代理:

self.webView = WKWebView(frame: self.view.bounds, configuration: configuretion)

self.webView.navigationDelegate = self
self.webView.UIDelegate = self
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

加載我們的本地HTML頁面:

let url = NSBundle.mainBundle().URLForResource("test", withExtension: "html")
self.webView.loadRequest(NSURLRequest(URL: url!))
self.view.addSubview(self.webView);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

我們再添加前進、後退按鈕和添加一個加載進度的控制顯示在Webview上:

self.progressView = UIProgressView(progressViewStyle: .Default)
self.progressView.frame.size.width = self.view.frame.size.width
self.progressView.backgroundColor = UIColor.redColor()
self.view.addSubview(self.progressView)

self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "上一個頁面", style: .Done, target: self, action: "previousPage")
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "下一個頁面", style: .Done, target: self, action: "nextPage")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

頁面前進、後退

對於前進後退的事件處理就很簡單的,要注意判斷一下是否可以後退、前進才調用:

func previousPage() {
    if self.webView.canGoBack {
      self.webView.goBack()
    }
}

func nextPage() {
    if self.webView.canGoForward {
      self.webView.goForward()
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

當然除了這些方法之處,還有重新載入等。

WKWebView的KVO

對於WKWebView,有三個屬性支持KVO,因此我們可以監聽其值的變化,分別是:loading,title,estimatedProgress,對應功能表示爲:是否正在加載中,頁面的標題,頁面內容加載的進度(值爲0.0~1.0)

// 監聽支持KVO的屬性
self.webView.addObserver(self, forKeyPath: "loading", options: .New, context: nil)
self.webView.addObserver(self, forKeyPath: "title", options: .New, context: nil)
self.webView.addObserver(self, forKeyPath: "estimatedProgress", options: .New, context: nil)
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

然後就可以重寫監聽的方法來處理。這裏只是取頁面的標題,更新加載的進度條,在加載完成時,手動調用執行一個JS方法:

// MARK: - KVO
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
  if keyPath == "loading" {
    print("loading")
  } else if keyPath == "title" {
    self.title = self.webView.title
  } else if keyPath == "estimatedProgress" {
    print(webView.estimatedProgress)
    self.progressView.setProgress(Float(webView.estimatedProgress), animated: true)
  }

  // 已經完成加載時,我們就可以做我們的事了
  if !webView.loading {
    // 手動調用JS代碼
    let js = "callJsAlert()";
    self.webView.evaluateJavaScript(js) { (_, _) -> Void in
      print("call js alert")
    }

    UIView.animateWithDuration(0.55, animations: { () -> Void in
      self.progressView.alpha = 0.0;
    })
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

WKUIDelegate

我們看看WKUIDelegate的幾個代理方法,雖然不是必須實現的,但是如果我們的頁面中有調用了js的alert、confirm、prompt方法,我們應該實現下面這幾個代理方法,然後在原來這裏調用native的彈出窗,因爲使用WKWebView後,HTML中的alert、confirm、prompt方法調用是不會再彈出窗口了,只是轉化成iOS的native回調代理方法:

// MARK: - WKUIDelegate
// 這個方法是在HTML中調用了JS的alert()方法時,就會回調此API。
// 注意,使用了`WKWebView`後,在JS端調用alert()就不會在HTML
// 中顯示彈出窗口。因此,我們需要在此處手動彈出ios系統的alert。
func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: () -> Void) {
  let alert = UIAlertController(title: "Tip", message: message, preferredStyle: .Alert)
  alert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (_) -> Void in
    // We must call back js
    completionHandler()
  }))

  self.presentViewController(alert, animated: true, completion: nil)
}

func webView(webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: (Bool) -> Void) {
  let alert = UIAlertController(title: "Tip", message: message, preferredStyle: .Alert)
  alert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (_) -> Void in
    // 點擊完成後,可以做相應處理,最後再回調js端
    completionHandler(true)
  }))
  alert.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: { (_) -> Void in
    // 點擊取消後,可以做相應處理,最後再回調js端
    completionHandler(false)
  }))

  self.presentViewController(alert, animated: true, completion: nil)
}

func webView(webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: (String?) -> Void) {
  let alert = UIAlertController(title: prompt, message: defaultText, preferredStyle: .Alert)

  alert.addTextFieldWithConfigurationHandler { (textField: UITextField) -> Void in
    textField.textColor = UIColor.redColor()
  }
  alert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (_) -> Void in
     // 處理好之前,將值傳到js端
    completionHandler(alert.textFields![0].text!)
  }))

  self.presentViewController(alert, animated: true, completion: nil)
}

func webViewDidClose(webView: WKWebView) {
  print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

WKScriptMessageHandler

接下來,我們看看WKScriptMessageHandler,這個是注入js名稱,在js端通過window.webkit.messageHandlers.{InjectedName}.postMessage()方法來發送消息到native。我們需要遵守此協議,然後實現其代理方法,就可以收到消息,並做相應處理。這個協議只有一個方法:

// MARK: - WKScriptMessageHandler
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
  print(message.body)
  // 如果在開始時就注入有很多的名稱,那麼我們就需要區分來處理
  if message.name == "AppModel" {
    print("message name is AppModel")
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這個方法是相當好的API,我們給js注入一個名稱,就會自動轉換成js的對象,然後就可以發送消息到native端。

WKNavigationDelegate

還有一個非常關鍵的代理WKNavigationDelegate,這個代理有很多的代理方法,可以控制頁面導航。

其調用順序爲: 
1、這個代理方法是用於處理是否允許跳轉導航。對於跨域只有Safari瀏覽器才允許,其他瀏覽器是不允許的,因此我們需要額外處理跨域的鏈接。

// 決定導航的動作,通常用於處理跨域的鏈接能否導航。WebKit對跨域進行了安全檢查限制,不允許跨域,因此我們要對不能跨域的鏈接
// 單獨處理。但是,對於Safari是允許跨域的,不用這麼處理。
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
    print(__FUNCTION__)

    let hostname = navigationAction.request.URL?.host?.lowercaseString

    print(hostname)
    // 處理跨域問題
    if navigationAction.navigationType == .LinkActivated && !hostname!.containsString(".baidu.com") {
      // 手動跳轉
      UIApplication.sharedApplication().openURL(navigationAction.request.URL!)

      // 不允許導航
      decisionHandler(.Cancel)
    } else {
      self.progressView.alpha = 1.0

      decisionHandler(.Allow)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2、開始加載頁面內容時就會回調此代理方法,與UIWebViewdidStartLoad功能相當

func webView(webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
    print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

3、決定是否允許導航響應,如果不允許就不會跳轉到該鏈接的頁面。

func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
    print(__FUNCTION__)
    decisionHandler(.Allow)
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

4、Invoked when content starts arriving for the main frame.這是API的原註釋。也就是在頁面內容加載到達mainFrame時會回調此API。如果我們要在mainFrame中注入什麼JS,也可以在此處添加。

func webView(webView: WKWebView, didCommitNavigation navigation: WKNavigation!) {
  print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

5、加載完成的回調

func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
  print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

如果加載失敗了,會回調下面的代理方法:

func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) {
  print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

其實在還有一些API,一般情況下並不需要。如果我們需要處理在重定向時,需要實現下面的代理方法就可以接收到。

func webView(webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
  print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

如果我們的請求要求授權、證書等,我們需要處理下面的代理方法,以提供相應的授權處理等:

func webView(webView: WKWebView, didReceiveAuthenticationChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    print(__FUNCTION__)
    completionHandler(.PerformDefaultHandling, nil)
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

當我們終止頁面加載時,我們會可以處理下面的代理方法,如果不需要處理,則不用實現之:

func webViewWebContentProcessDidTerminate(webView: WKWebView) {
    print(__FUNCTION__)
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

源代碼

具體代碼已經發布到github:https://github.com/CoderJackyHuang/WKWebViewTestDemo

總結

蘋果已經向我們提供了WKWebView,擁有UIWebView的所有功能,且還提供更多的功能,明示是爲了替代UIWebView,但是WKWebView要在ios8.0之後才能使用,因此,如果我們使用Swift來開發應用,兼容版本從8.0開始時,可以直接使用WKWebView

我們可以發現,蘋果提供了更多簡便的方式讓native與js交互更加方便,通過讓native注入名稱,然後在js端自動轉換成js的對象,就可以在js端通過對象的方式來發送消息到native端。如此一來,就簡化了js與native的交互了。

關注我

公衆號搜索「ios開發技術分享」快速關注微信號:iOSDevShares QQ羣:324400294

發佈了40 篇原創文章 · 獲贊 11 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章