好像只有在亞洲地區才流行這樣的組合,也就是從 native app 畫面開 web view 切到行銷活動網頁,再透過 javascript 方式跟 app 互動之後回到 app 裡進行後面的導頁與串行銷資料。
同步切換 app 畫面標題
為了要讓 webView 載入的網頁標題也能貼回到 App 裡的 navigation bar 上,所以在 viewDidLoad()
的時候,加上 addObserver
:
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: .new, context: nil)
為了避免 leak,若有開啟 addObserver
更新 title 的話,在 ViewController dismiss 時要移除掉:
webView.removeObserver(self, forKeyPath: "title")
顯示與取消 Loading
對應到 WKNavigationDelegate
可以透過下方兩個 methods 顯示或取消自訂的 loading view:
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
guard isBeingDismissed || isMovingFromParent else {
// present loading view
return
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// dismiss loading view
}
同理,通常也會在 didFinish
的時候埋一個 callback function 用來觸發自訂的行為:
var pageIsLoaded: (() -> Void)?
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// dismiss loading view
self.pageIsLoaded?()
}
檢查回應 status code
如下例檢查 status code 不是 200 類則進行後續行為:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
if let response = navigationResponse.response as? HTTPURLResponse {
if (200 ..< 300 ~= response.statusCode) == false {
// present alert view
}
}
decisionHandler(.allow)
}
處理自訂錯誤
如下例處理自訂錯誤:
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
// handle error
}
加上自訂 WKWebViewConfiguration
需要在 WKWebView
生成之前先將 WKWebViewConfiguration
建立好,才能夠在 WKWebView
建立時候傳入。
建立的方式如下:
private var customConfiguration: WKWebViewConfiguration?
private weak var scriptMessageHandler: WKScriptMessageHandler?
private func setupWebViewConfiguration() {
self.scriptMessageHandler = self
if let handler = scriptMessageHandler {
let contentController = WKUserContentController()
contentController.add(handler, name: WebViewScriptHandlerName.ncbmb.rawValue)
self.customConfiguration = WKWebViewConfiguration()
self.customConfiguration?.userContentController = contentController
}
webView = WKWebView(frame: .zero, configuration: self.customConfiguration ?? WKWebViewConfiguration())
webView.clearCache()
webView.backgroundColor = .white
webView.scrollView.backgroundColor = .white
webView.uiDelegate = self
webView.navigationDelegate = self
}
會需要把 scriptMessageHandler
另外拉出來是因為會有 leak 問題,所以 ViewController 在 dismiss 前要先把 handler 釋放掉,通常我們是做成 func 讓 coordinator 呼叫:
func dismissWebVC() {
self.dismiss(animated: false) { [weak self] in
self?.webView?.configuration.userContentController.removeScriptMessageHandler(forName: WebViewScriptHandlerName.ncbmb.rawValue)
self?.scriptMessageHandler = nil
if self?.intent?.followWebTitle == true {
self?.webView?.removeObserver(self!, forKeyPath: "title")
}
}
}
與 WebView 裡的 Javascript 互動
由 App 觸發執行 javascript 給 WebView 裡的網頁方式如下,就會觸發 WebView 裡的 sendMsg
function:
func sendMessage(message: ScriptBaseMessage) {
guard let json = message.toJsonString() else { return }
self.webView.evaluateJavaScript("sendMsg('\(json)')",
completionHandler: { _, _ in
})
}
而從 WebView 端收回 json 資料則需要 WKScriptMessageHandler
protocol 的實作:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let msg = self.getData(ScriptCommandMessage.self, message: message) {
self.receivedCommand(postMessage: msg)
}
}
private func getData<T: ScriptBaseMessage>(_ type: T.Type, message: WKScriptMessage) -> T? {
guard let body = message.body as? String else { return nil }
guard let postMessage = try? JSONDecoder().decode(T.self, from: Data(body.utf8)) else { return nil }
return postMessage
}
private func receivedCommand<T: ScriptBaseMessage>(postMessage: T) {
// your message here
}
然後因為收到的訊息為了保持彈性設計,所以使用了 Generic 泛型來處理不同情境的資料。
大概就是這樣,實務上會碰到的眉角其實也不少,很多狀況也都是碰到了才有處理的經驗。