有的時候一些跨平台共用的頁面會使用網頁的方式打造,在 iOS 的世界裡我們可以使用 WKWebView 呈現網頁內容,除了單純的呈現之外,彼此的互動也是不可或缺的一環。這篇文章將會簡單介紹該如何達成網頁與 iOS 原生程式碼之間的雙向溝通。

如何從 Web 呼叫 Native 的函式

我們事先要約定好可被呼叫的函式名稱,以及可接受的參數格式,然後在 Web 呼叫 Javascript 程式如下:

window.webkit.messageHandlers.myAwesomeHandler.postMessage({
    param1: "value1",
    param2: "value2"
})

根據蘋果官方文件的說明,可被呼叫的函式都要放在 window.webkit.messageHandlers 物件底下,其中 myAwesomeHandler 是雙方約定好的函式名稱,postMessage 要傳的則是約定好的參數格式,最常見的格式就是 JSON ObjectString

如何在 Native 回應 Web 的呼叫

上一段提到 Web 要呼叫的 handler 是雙方約定好的函式名稱,我們可以像這樣列舉所有支援的 handlers:

private enum WebMessageHandler: String, CaseIterable {
    case myAwesomeHandler
    case iapHandler
    case closeHandler
}

假設我們自訂一個 UIViewController 來顯示 WKWebView,在 viewDidLoad() 裡頭可用以下做法告訴 webView 有哪些 message handlers 能夠被呼叫:

let configuration = WKWebViewConfiguration()
for handler in WebMessageHandler.allCases {
    configuration.userContentController.add(self, name: handler.rawValue)
}
webView = WKWebView(frame: .zero, configuration: configuration)
view.addSubview(webView)

另外要注意的是,因為 userContentController 會持有 message handler,所以要記得在適當時機呼叫 removeScriptMessageHandler(forName name: String),這樣才不會產生 retain-cycle 造成 memory leak。或者也可以參考這個 stack overflow 的討論使用其他方法解決。

Note:

除了列出所有會用到的函式之外,還有另一種作法是只約定一個函式,再根據傳過來的參數判斷要作什麼事情。

別忘了在我們自訂的 UIViewController 要遵守 WKScriptMessageHandler 協定,這樣才能回應 Web 的請求:

extension MyUIViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let handler = WebMessageHandler(rawValue: message.name) else { return }
        switch handler {
        case .myAwesomeHandler:
            guard let dict = message.body as? [String: Any],
                let param1 = dict["param1"] as? String,
                let param2 = dict["param2"] as? String else {
                    return
                }
            // Do some awesome stuff.
        case .iapHandler:
            // Open IAP page.
        case .closeHandler:
            // Close the web view.
        }
    }
}

如何從 Native 呼叫 Web 的函式

當我們處理完畢 Web 的請求之後,要如何把結果傳回去呢?一個很常見的作法是透過 JSON String,它的程式碼大致上像這樣:

let data: [String: Any] = [
    "type": "userInfo",
    "value": [
        "name": "John Appleseed",
        "id": "johnappleseed12345678"
    ]
]

guard let json = try? JSONSerialization.data(withJSONObject: data, options: [.withoutEscapingSlashes]),
      let jsonString = String(data: json, encoding: .utf8)
else {
    return
}

let javascript = "window.actions.MessageFromNative('\(jsonString)')"
webView.evaluateJavaScript(javascript, completionHandler: nil)

其中 window.actions.MessageFromNative() 是雙方事先約定好在 Web 端要實作的 Javascript 函式,如此一來 Native 只要把資料轉成 JSON String 再傳過去即可,當然 JSON 的格式也是事先約定好的。

Note:

如果是傳 JSON String 的話,很有可能需要開啟 .withoutEscapingSlashes 設定,這樣對方才不會收到反斜線跳脫字元。

除了 JSONSerialization 也可以用 JSONEncoder,挑適合自己情境的來使用即可。

以上簡單的說明了 Native Code 跟 Web 之間的交互過程,以此為基礎再加上一些創造力,就可以玩出很多花樣了!