由於 UIWebView
已經過時一段時間了,Apple 建議改用 WKWebView
(iOS 8+) 來實現 webview 的呈現,這篇就來簡單做個介紹跟可能會遇到的問題解法。
WKWebView
是 WebKit 的一個內建元件,可在原生 App 裡內嵌一個開啟網頁的 view,除了一般的 load(_:)
, reload()
, goBack(_:)
, goForward(_:)
, estimatedProgress
等等之外,還可以做一些設定跟進階判斷:
- 可設定 WKWebViewConfiguration 做更細部的設定
- 可實作
WKUIDelegate
來實現 JS 在瀏覽器網頁的功能
- 實現 JS alert, confirm, prompt
- 實作 target = _blank (另開分頁) 功能
- 規範 webview 的
WKNavigationDelegate
自訂是否要允許或拒絕導航目標 URL 的政策。
- 自訂導航政策
- 決策 URL 是開啟原生畫面或 webview
初始化 WKWebView
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
lazy var webConfiguration: WKWebViewConfiguration = {
let pref = WKPreferences()
pref.javaScriptEnabled = true
let config = WKWebViewConfiguration()
config.preferences = pref
return config
}()
func setupWebView() {
webView = WKWebView(frame: .zero, configuration: configuration)
webView.uiDelegate = self
webView.navigationDelegate = self
self.view.addSubview(webView)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
let myURL = URL(string:"https://www.apple.com")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest)
}
}
WKUIDelegate
實現 JS alert, confirm, prompt
Web 很常使用到 js 的 alert, confirm 或是 prompt 的功能,但無法觸發,因為 WKWebView 把這些事件攔截下來,傳入這個 delegate 中,讓開發者使用 App 的原生元件來進行控制
// 跳出視窗
alert("hello world");
// confirm 確認訊息
var c = confirm('Are your sure?');
// prompt 跳出輸入框對話
var p = prompt(message, default);
可實作下列 WKUIDelegate
的 methods 來實現上述的功能
處理 JS alert()
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "確定", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
completionHandler()
}
處理 JS confirm()
// `completionHandler()` 帶 `true` 的話等是確定(OK), 帶 `false` 則是取消(Cancel)
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "確定", style: .default, handler: { (action) in
completionHandler(true)
}))
alertController.addAction(UIAlertAction(title: "取消", style: .default, handler: { (action) in
completionHandler(false)
}))
self.present(alertController, animated: true, completion: nil)
}
處理 JS prompt()
// webView(_:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:)
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
let alertController = UIAlertController(title: nil, message: prompt, preferredStyle: .alert)
// 在 alertController 上加入一個 TextField
alertController.addTextField { (textField) in
textField.text = defaultText
}
alertController.addAction(UIAlertAction(title: "確定", style: .default, handler: { (action) in
if let text = alertController.textFields?.first?.text {
completionHandler(text)
} else {
completionHandler(defaultText)
}
}))
alertController.addAction(UIAlertAction(title: "取消", style: .default, handler: { (action) in
completionHandler(nil)
}))
self.present(alertController, animated: true, completion: nil)
}
實作 target = _blank (另開分頁) 功能
可實作
webView(_:createWebViewWith:for:windowFeatures:)
來判斷 web 的 target = _blank
(另開分頁) ,接著可以自行決定是要透過原本的 webview 來繼續瀏覽目標網頁或是再另外 present 一個 view 出來
// WKNavigationDelegate
func webView(webView: WKWebView!, createWebViewWithConfiguration configuration: WKWebViewConfiguration!, forNavigationAction navigationAction: WKNavigationAction!, windowFeatures: WKWindowFeatures!) -> WKWebView! {
// 判斷 targetFrame 是否等於 nil,若是則為另開分頁
if navigationAction.targetFrame == nil {
// 這邊的實作是由原本的 webview 再繼續開啟另開分頁的 URL
webView.loadRequest(navigationAction.request)
}
return nil
}
WKNavigationDelegate
自訂導航政策
webView(_:decidePolicyFor:preferences:decisionHandler:)
可以用來自訂是否允許或拒絕將要瀏覽的目標 URL,若實作此 methold 請記得在特定的點執行 decisionHandler
,另可判斷 preferences
的
WKNavigationType
有些哪類型可以調用
決策 URL 是開啟原生畫面或 webview
假設有個情境是透過點擊連結之後判斷是否要由 native app 開啟或由 webview 或由 Safari browser 來開啟頁面,如果遇到自家的網域時可以開啟原生畫面,其他則是開啟 webview 或 browser
// 可先定義一個 open url type 的 enum
enum OpenUrlType {
/// 原生頁面
case native
/// webview
case webview
/// 使用 safari browser
case browser
}
決定開啟的方式
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: ((WKNavigationActionPolicy) -> Void)) {
...
// 可透過 switch 判斷 navigationType .linkActivated 或 .other 等等
switch navigationAction.navigationType {
case .linkActivated: // user 點擊的連結
// 再來取得要開啟的 OpenUrlType 是什麼
if let type = getOpenUrlType(URL: url) {
if type == .browser { // 使用外部 safari browser 開啟連結
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
return
} else if type == .native { // 原生頁面
// do something
// 做一些開啟原生畫面的實作
decisionHandler(.cancel)
return
} else if type == .webview { // webview
// 由此 webview 物件繼續瀏覽 URL
decisionHandler(.allow)
return
}
}
case .other: // 其他
// do something
...
decisionHandler(.allow)
}
// getOpenUrlType 用來決定 url 是要透過什麼方式開啟
private func getOpenUrlType(URL url: URL) -> OpenUrlType? {
guard
let components = NSURLComponents(url: url as URL, resolvingAgainstBaseURL: true),
let host = components.host,
let pathComponents = components.path?.components(separatedBy: "/")
else {
return .browser
}
// 判斷網域是否為自家的網域
if matchDomain(domain: host, in: ["example.com.tw", "example.tw"]) {
if pathComponents.count >= 3 {
// do something
return .native
}
// 再自家網域內,但沒原生畫面,改為 webview 開啟
return .webview
}
return .browser
}