WKWebView 遇到 target="_blank" 的處理

如果有發現程式碼有問題歡迎留言告訴我！

## 前言

因為點了 WKWebView 中網頁的超連結發現他沒有反應，才發現這需要另外處理。

## 環境

Xcode 11.3, Swift 5, Storyboard

## 一些些前導知識

普通的超連結不需要手動處理， WKWebView 就會自己幫我們導到下一個頁面了。

### target="_blank"

是 HTML 裡面 a （錨點, anchor） 標籤中，用來指定這個超連結應該要開一個新的視窗。

如果不加這個屬性，如下，就會在同一個頁面導向下個頁面。

``` html
<a href="https://apple.com">Apple</a>
```

如果加了該屬性，原先的頁面就不會有任何動作，在一般的瀏覽器就會幫你開一個新的視窗。

``` html
<a href="https://apple.com" target="_blank">Apple</a>
```

#### 參照

https://www.w3schools.com/tags/att_a_target.asp

> _blank
> Opens the linked document in a new window or tab

## 範例

在 Xcode 開一個新專案來試試看，在 Storyboard 裡面放一個 WKWebView 進去，並把他設定 IBOutlet 到所屬的 view controller 。

這裡就略過 Storyboard 裡面放了什麼，直接放初始化的程式碼。

``` swift
import UIKit
import WebKit

class ViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView.navigationDelegate = self
        webView.loadHTMLString(htmlString, baseURL: nil)
    }
}

extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        // 在這裡先讓他執行預設的行為
        decisionHandler(.allow)
    }
}

extension ViewController {
    /// 實際內容
    var htmlString: String {
        return """
            <h1><a href="https://apple.com" target="_blank">Apple</a></h1>
            <h1><a href="https://apple.com">Apple</a></h1>
        """
    }
}
```

實際的內容很簡單，就是兩個 Apple 的超連結，一個是有指定要新開頁面，另一個則是直接在原先的頁面跳轉。放 h1 的原因是 web view 中預設的字級會超小，直接用 h1 讓他成為大標變大字。

``` html
<h1><a href="https://apple.com" target="_blank">Apple</a></h1>
<h1><a href="https://apple.com">Apple</a></h1>
```

### 在 WKWebView 中的行為

到這裡先執行看看，
就會發現第一個超連結點下去之後他不會進行任何的跳轉， console 中還會出現像是這樣的錯誤訊息。
這個就是我們在這邊要解決的問題。

```
Unknown result for URL 0x2832bc0e0 (https)
```

而第二個超連結點下去則會順利的導向 Apple 的首頁。

## 得知按了一個連結和操作

在上面初始化的程式碼裡面也有寫出來，WKNavigationDelegate 中，有方法可以監聽，並作出對應的處理。在這邊就是使用這個方法。

``` swift
optional func webView(_ webView: WKWebView, 
      decidePolicyFor navigationAction: WKNavigationAction, 
      decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
```

https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview

在這個方法裡面，可以對 decisionHandler 傳入不同的值來告訴他該不該繼續跳轉或者是說進行預設的行為。 `.allow` 則是預設的行為。

``` swift
decisionHandler(.allow)
```

### 取得這個超連結的內容並處理

我們可以透過第二個參數來取得超連結的內容，而在這邊我們想要看的就是和 `target` 屬性有相關的參數，也就是 `targetFrame`

``` swift
navigationAction.targetFrame
```

https://developer.apple.com/documentation/webkit/wknavigationaction/1401918-targetframe

如文件裡面的敘述：

> The target frame, or nil if this is a new window navigation.

就可以知道，如果是超連結本身指定要開新視窗的話，這個屬性的值就會是 nil 。

### 檢查 targetFrame

因此這個方法的內容就可以改成這樣：

``` swift
extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        
        // 為求範例單純化這邊只檢查 targetFrame ，
        // 實務上還是需要加上檢查 url 是不是合法可以開啟等等處理
        if navigationAction.targetFrame == nil {
            // 傳入 .cancel 停止 navigation 預設的行為
            decisionHandler(.cancel)

            // 這裡就是開始做想做的事情， webView.load 可以直接讓當前的 webView 導向目標頁面
            // 當然也可以開啟 SFSafariViewController 或是開啟 Mobile Safari / Mobile Chrome
            webView.load(navigationAction.request)

            // 呼叫過之後 devisionHandler 之後就在這邊讓這個方法結束
            return
        }
        
        decisionHandler(.allow)
    }
}
```

#### 注意點

decisionHandler 呼叫第二次（及以上）會 crash 

## 最後的程式碼

``` swift
import UIKit
import WebKit

class ViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView.navigationDelegate = self
        webView.loadHTMLString(htmlString, baseURL: nil)
    }
}

extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        
        if navigationAction.targetFrame == nil {
            decisionHandler(.cancel)
            webView.load(navigationAction.request)
            return
        }
        
        decisionHandler(.allow)
    }
}

extension ViewController {
    var htmlString: String {
        return """
            <h1><a href="https://apple.com" target="_blank">Apple</a></h1>
            <h1><a href="https://apple.com">Apple</a></h1>
        """
    }
}
```

## 參考

- 官方文件
    - https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview
    - https://developer.apple.com/documentation/webkit/wknavigationaction
    - https://developer.apple.com/documentation/webkit/wknavigationaction/1401918-targetframe
- https://stackoverflow.com/a/40742587
    - 有提到處理 `target="_blank"`
- https://stackoverflow.com/a/39724007
    - 有提到怎麼處理超連結