「The Ultimate Guide to WKWebView」をSwiftUIで実装する #06 - Opening a link in the external browser -

[「The Ultimate Guide to WKWebView」](https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebvie)をSwiftUIで実装してみるの、
６つ目になります。

前回に続き、WebViewで押下されたリンクの文字列を元に色々やります！
タイトルは「Opening a link in the external browser」ですが、
外部ブラウザでリンクを開くだけでなく、それ以外にも色々やってます。

もはや元記事は名前を借りているだけになってきたかも・・・

## 目次

シリーズ化していこうと思うので、全体の目次を置いておきます。
リンクが貼られていないタイトルは、記事作成中または未作成のものになります。

|#|タイトル|
|------|-----------------|
|01|[Making a web view fill the screen](https://qiita.com/kamimi01/items/b4d6cbafa587831c1be0) <br> （WebViewを画面に表示する）|
|02|[Loading remote content](https://qiita.com/kamimi01/items/c889979964820d5f9fa2) <br> （リモートのコンテンツを読み込む）|
|03|[Loading local content](https://qiita.com/kamimi01/items/9cefe1e73ab5b88051c6) <br> （ローカルのコンテンツを読み込む）|
|04|[Loading HTML fragments](https://qiita.com/kamimi01/items/2f267b328f3f6158bc26) <br> （HTMLフラグメントの読み込み）|
|05|[Controlling which sites can be visited](https://qiita.com/kamimi01/items/7c01aed497108da35b5a) <br> （訪問可能なサイトの制御）|
|06|[Opening a link in the external browser](https://qiita.com/kamimi01/items/716dccb6f109ee50c900) <br> （外部ブラウザでリンクを開く）|
|07|[Monitoring page loads](https://qiita.com/kamimi01/items/33446e032822518cb7f7) <br> （ページの読み込みを監視する）|
|08|[Reading a web page’s title as it changes](https://qiita.com/kamimi01/items/7e0153ee68474a155b9a) <br> （Webページのタイトルの変化を読み取る）|
|09|[Reading pages the user has visited](https://qiita.com/kamimi01/items/f5f6c35a6a68726fa485) <br> （ユーザーが閲覧したページを読み取る）|
|10|[Injecting JavaScript into a page](https://qiita.com/kamimi01/items/6076081dc54e2b3d0abf) <br> （JavaScriptをページに注入する）|
|11|[Reading and deleting cookies](https://qiita.com/kamimi01/items/90139fe692ec988d28bb) <br> （cookieの読み取りと削除）|
|12|[Providing a custom user agent](https://qiita.com/kamimi01/items/5884330e315338df8ec0) <br> （カスタムUser Agentを提供する）|
|13|[Showing custom UI ](https://qiita.com/kamimi01/items/3315e4df0813eda75eeb)<br> （カスタムUIを表示する）|
|14|Snapshot part of the page <br> （ページの一部のスナップショットを撮る）|
|15|Detecting data <br> （データの探索）|

## 環境

【Xcode】13.1
【Swift】5.5
【iOS】15.0
【macOS】Big Sur バージョン 11.4

## 実現したいこと

今回やることは
WebViewにある`http`または`https`がスキームのリンクを押下すると、Safariに飛んでそっちでページを表示します。
遷移が成功したら、アプリ側ではアラートを出しています。

で、カスタムURLスキームの方のリンク（スキームが`sample-app`）を押下すると、そのカスタムURLスキームが設定されたアプリに飛びます。
こちらも同じく遷移に成功すると、アプリ側でアラートを出します。

今回は自分自身に設定された`sample-app`というカスタムURLスキームをフックしているので、どこにも遷移してないように見えますが、
訳あってこうしてます。（後ほど説明します）

GIFの最後の方で、ちゃんと自分自身にカスタムURLスキームが設定されていることを示すために、
Safariからアプリに飛んでいる様子も一緒に動画に撮っています。

![app](https://user-images.githubusercontent.com/47489629/153602527-6f0c7464-5452-4a67-b1a8-b42012993348.gif)

画像サイズをなるべく小さく抑えるため、かなり高速に操作しているので、見づらかったらすみません・・・ :bow: 

## 実現方法

まずWebViewです。

特定のリンクの検知を行なっているのは、前回に同じく
Coordinatorクラスにある`webView(_:decidePolicyFor:decisionHandler:)`メソッドです。

で、今回のメインは、 **`open(_:options:completionHandler:)`メソッド** です。
これは指定されたURLを非同期で開くためのメソッドです。

`http`または`https`がスキームになっているリンクがきた場合と
`sample-app`がスキームになっているリンクがきた場合で条件分岐していますが

その先で使用しているメソッド自体は同じです。
アラートに表示するメッセージを変えたかったので、条件分岐しているだけです。

ちなみにHacking with Swiftでは、Safariで開くリンクの説明だけがされていました。

```swift:WebView.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: String
    @ObservedObject var viewModel: WebViewModel

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.navigationDelegate = context.coordinator

        guard let url = URL(string: url) else {
            return
        }
        let request = URLRequest(url: url)
        uiView.load(request)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(viewModel: viewModel)
    }
}

extension WebView {
    class Coordinator: NSObject, WKNavigationDelegate {
        var viewModel: WebViewModel

        init(viewModel: WebViewModel) {
            self.viewModel = viewModel
        }

        // decisionHandlerを実行しないケースがあると、クラッシュするので注意
        func webView(
            _ webView: WKWebView,
            decidePolicyFor navigationAction: WKNavigationAction,
            decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
        ) {
            guard let url = navigationAction.request.url,
                  let scheme = url.scheme
            else {
                decisionHandler(.cancel)
                return
            }

            switch scheme {
            case "http", "https":
                guard let host = url.host else {
                    decisionHandler(.cancel)
                    return
                }
                // ホストがlocalhostのページだけは、アプリで開く（初期表示時の画面）
                if host == "localhost" {
                    decisionHandler(.allow)
                    return
                }

                // localhost以外はSafariで開く
                UIApplication.shared.open(url) { [weak self] result in
                    guard let self = self else {
                        decisionHandler(.cancel)
                        return
                    }
                    self.viewModel.alertTitle = "Safariで開いた"
                    self.viewModel.isShownAlert = result
                }
                decisionHandler(.cancel)
            case "sample-app":
                // 自分自身のカスタムURLスキームを指定する
                UIApplication.shared.open(url) { [weak self] result in
                    guard let self = self else {
                        decisionHandler(.cancel)
                        return
                    }
                    self.viewModel.alertTitle = "カスタムURLで自分自身を開いた"
                    self.viewModel.isShownAlert = result
                }
                decisionHandler(.cancel)
            default:
                decisionHandler(.cancel)
            }
        }
    }
}
```

公式ドキュメントによると、`open(_:options:completionHandler:)`メソッドの
第一引数`url`の説明の中に、この記述がありました。

「UIKitは、http、https、tel、facetime、mailtoの多くのスキームをサポートしています。デバイスにインストールされたアプリに関連付いているカスタムURLスキームに対しても動作します。」

> UIKit supports many common schemes, including the http, https, tel, facetime, and mailto schemes. You can also employ custom URL schemes associated with apps installed on the device.

私はHacking with Swiftのタイトルを見て、外部ブラウザで開くためのメソッドだと思っていたのですが、それ以外にもたくさんサポートされてました。
多分私みたいに勘違いする人もいると思うので、いろんなスキームがサポートされていること、Hacking with Swiftの方にも書いといた方が良いんじゃないかなと思いました。
（やっぱり公式ドキュメントはちゃんと読んどかないといかん・・・）

というわけで今回は、

* `https`から始まりSafariで開くリンクと、
* カスタムURLスキーム`sample-app`から始まりそのスキームが設定されたアプリ（今回は自分自身）を開くリンク

を用意して検証しました。

次に気になるのが、第三引数の`completionHandler`。
公式の説明を見てみると、こうあります。（英文長いので少し意訳してます。）

「URLを開いたことが成功したか失敗したかを知らせたい場合に、このパラメータで値を渡します。このブロックはメインスレッドで非同期的に実行されます。」

> The block to execute with the results. Provide a value for this parameter if you want to be informed of the success or failure of opening the URL. This block is executed asynchronously on your app's main thread. The block has no return value and takes the following parameter:

> success
> A Boolean value that indicates whether the URL was opened successfully.

今回はこのBool値を元に、URLを開くことが成功したらアラートを表示するように実装しています。

ちなみになぜ、自分自身のカスタムURLスキームを設定したのかというと、、、
理由は単純で仕事で実際こういうケースに当たったことがあるからです。 :sweat: 
（大した理由じゃなくてすみません）

まああと、今回の場合は単純にもう１アプリ実装するのが面倒だったというのもありますね。

カスタムURLスキームはこんな感じで設定しています。

![スクリーンショット 2022-02-11 23.13.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/fae49954-6ca7-86c2-69e1-8998d2112f7b.png)

前回に引き続き結構、カスタムURLスキームを使っているのですが、
Apple公式としては、ディープリンクの機能を実装する上では、**カスタムURLスキームよりもユニバーサルリンクの使用を強く推奨**しています。

カスタムURLスキームを使用する場合は、
不正なURLがきた時の処理をきちんとやってね、とばっちり書いてあります。（雑・・・）

> URL schemes offer a potential attack vector into your app, so make sure to validate all URL parameters and discard any malformed URLs. In addition, limit the available actions to those that don’t risk the user’s data. For example, don’t allow other apps to directly delete content or access sensitive information about the user. When testing your URL-handling code, make sure your test cases include improperly formatted URLs.

がまあしかし、カスタムURLスキームを設定するのは外部アプリになりますし、
いやいきなりユニバーサルリンクにしろって言われてもムリ！な場合もあると思います。

私の場合はムリでした。そういう致し方ない場合もあります。

ちなみに今回表示したHTMLはこちら。
前回とあまり変わりないと思います。

```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>HTMLを表示するテスト</h1>
    <a href="https://www.apple.com/">AppleのホームページをSafariで表示</a><br />
    <a href="sample-app://">ネイティブアプリでアラートを表示</a> 
  </body>
</html>
```

---

では次にWebViewを使うViewです。
アラート画面の実装はここにされています。

```swift:ContentView
import SwiftUI

struct ContentView: View {
    private let url = "http://localhost:3000"
    @ObservedObject private var viewModel = WebViewModel()

    var body: some View {
        WebView(url: url, viewModel: viewModel)
            .alert(viewModel.alertTitle, isPresented: $viewModel.isShownAlert) {
                Button(action: {}) {
                    Text("OK")
                }
            }
    }
}
```

で、今回も前回と同じく、ViewModelクラスを実装しています。
アラート画面のための変数をいくつか持ってるだけです。

```swift:WebViewModel.swift
import Foundation

class WebViewModel: ObservableObject {
    @Published var isShownAlert = false
    @Published var alertTitle = ""
}
```

以上です！

コード全体は以下になります。

https://github.com/kamimi01/TheUltimateGuidetoWKWebViewWithSwiftUI/tree/main/OpeningALinkInTheExternalBrowser

今回もHacking with Swiftの元の記事にあるコードの原型留めてないですね。:innocent:

`open(_:options:completionHandler:)`メソッドの説明を色々読んでいたら、
こんなこともできるんだーと色々発見があって、だいぶ楽しかったです。

ちなみに本当に蛇足ですが、WebView内のリンクを検知するということではなく、
単純に外部ブラウザで開くためのViewをSwiftUIで作るときは、「[Link](https://developer.apple.com/documentation/swiftui/link)」っていうのがあります。（コードはあげてないけど、実は今回そっちも使ってみた）

たった１つのメソッドだけでもここまで学びがあるので、このシリーズ楽しいですね。 :blush:
いつも通り、コメント等は歓迎です！

## 参考

- [The Ultimate Guide to WKWebView](https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebvie)
- [`open(_:options:completionHandler:)`Apple公式ドキュメント](https://developer.apple.com/documentation/uikit/uiapplication/1648685-open)
- [Defining a Custom URL Scheme for Your App](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app)
- [[iOS] ディープリンク（Custom URL Scheme）でアプリを起動する](https://dev.classmethod.jp/articles/ios-custom-url-scheme/)
- [`Link`Apple公式ドキュメント](https://developer.apple.com/documentation/swiftui/link) →今回直接関係ないけど、SwiftUIで外部ブラウザへの遷移を実装したいときに使う