「The Ultimate Guide to WKWebView」をSwiftUIで実装する #02 -  Loading remote content -

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

多少SwiftUIとWebViewに関係ない、というかiOS開発に関係ないコードも入っちゃってますが、
そういうのに触れるのも好きなので、そこら辺も混ぜ込んでます。

必要なところだけお読みください。



## 目次

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

|#|タイトル|
|------|-----------------|
|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

## 実現したいこと

今回やることは
まずTextFieldに文字を入力します。

そしてサーバー側にその文字列を送ります。
サーバー側はその文字列を埋め込んだHTMLを返します。

クライアントはそのHTMLを取得し、WebViewで表示します。

こんな感じになりました。

![app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/3c5250b1-1f61-69de-dd97-59835221570d.gif)

（なぜかわからんが、画像の下が黒くなった・・・）

## 実現方法

まずWebViewです。

Hacking with Swiftの方では、`URLRequest`型のパラメータ`url`にURLを入れるだけでした。
でも最近やった例がそれだとダメなパターンだったので、記録がてらそっちを残しておきます。

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

struct WebView: UIViewRepresentable {
    let name: String
    
    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        let request = HTMLRequest(parameters: ["name": name]).buildURLRequest()
        uiView.load(request)
    }
}
```

`URLRequest(url: URL(String: "開きたいURL")!)`だと
なぜダメだったかというと、

* HTTPメソッドが`POST`だった
* Content-Typeが`x-www-form-urlencoded`だった
* 特定のデータをサーバーに送りたかった

があったためです。

なので、自分でそれに対応できるURLRequest型を自分で作ることになりました。

ちなみにこれは、WebViewとかSwiftUIとかあまり関係ない話です。

こういうことをやったよという自分の記録がてら、
あとシンプルな例にすると前回の＃1の記事と内容が丸かぶりする
のでわざと別の例を持ってきてます。

というわけで、こんな感じにURLRequestを作ります。

```swift:HTMLRequest.swift
import Foundation

class HTMLRequest {
    let baseURL = URL(string: "http://localhost:3000")!
    let method = "POST"
    let headers: [String: String] = ["Content-Type": "application/x-www-form-urlencoded"]
    var parameters = [String: String]()

    init(parameters: [String: String]) {
        self.parameters = parameters
    }

    func buildURLRequest() -> URLRequest {
        var urlRequest = URLRequest(url: baseURL)
        urlRequest.httpMethod = method
        headers.forEach { key, value in
            urlRequest.addValue(value, forHTTPHeaderField: key)
        }
        var components = URLComponents()
        components.queryItems = [URLQueryItem]()
        parameters.forEach {
            components.queryItems?.append(
                URLQueryItem(name: $0.key, value: $0.value)
            )
        }
        urlRequest.httpBody = components.query?.data(using: .utf8)
        return urlRequest
    }
}
```

最後に、WebViewを表示するViewです。

TextFieldが１つ、WebViewを開くためのボタンが１つあります。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var inputText = ""
    @State private var isShownWebView = false

    var body: some View {
        VStack {
            TextField("", text: $inputText)
                .border(.yellow)
                .padding()
            Button(action: {
                isShownWebView.toggle()
            }) {
                Text("モーダル開く")
            }
        }
        .fullScreenCover(isPresented: $isShownWebView) {
            WebView(name: inputText)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

以上です。

全体のソースコードはこちらに上がっています。

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

## その他調べたこと

WebViewやSwiftUI自体とは関係ないですが、調べたこと、やったことを書いておきます。

### URLReqeustについて

実は今まで`URLRequest(url: URL(String: "開きたいURL")!)`だと具体的にどんな
リクエストになるのか、ちゃんと見たことがなかったです・・・

まあGETだろう、くらいでしょうか。

なので今回は、lldbでpoして見てみました。
（ちなみにlldbは、「low-level debugger」のこと。poは、「print object」の略だそうです。後者は知ってたのですが、前者は知らなかった・・・）

まず`URLRequest(url: URL(String: "https://www.apple.com")!)`はこうなっていました。

```
(lldb) po request
▿ https://www.apple.com
  ▿ url : Optional<URL>
    ▿ some : https://www.apple.com
      - _url : https://www.apple.com
  - cachePolicy : 0
  - timeoutInterval : 60.0
  - mainDocumentURL : nil
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "GET"
  - allHTTPHeaderFields : nil
  - httpBody : nil
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : false
```

で、今回作ったURLRequestの方はというと、こうなっていました。

```
(lldb) po request
▿ http://localhost:3000
  ▿ url : Optional<URL>
    ▿ some : http://localhost:3000
      - _url : http://localhost:3000
  - cachePolicy : 0
  - timeoutInterval : 60.0
  - mainDocumentURL : nil
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "POST"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 1 element
      ▿ 0 : 2 elements
        - key : "Content-Type"
        - value : "application/x-www-form-urlencoded"
  ▿ httpBody : Optional<Data>
    ▿ some : 9 bytes
      - count : 9
      ▿ pointer : 0x00007ffeec1b6b78
        - pointerValue : 140732859640696
      ▿ bytes : 9 elements
        - 0 : 110
        - 1 : 97
        - 2 : 109
        - 3 : 101
        - 4 : 61
        - 5 : 84
        - 6 : 101
        - 7 : 115
        - 8 : 116
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : false
```

大体思ってた通りな感じかな？
前者は`GET`で
後者は、まあメソッド以外にも色々変わってます。（雑）

### APIの用意

この↓条件にあてはまるAPIがそこらへんにあればそれを使おうと思っていたのですが
すぐに見つかりそうになかったので、今回はAPIも自分で用意しました。

* HTTPメソッドが`POST`だった
* Content-Typeが`x-www-form-urlencoded`だった
* 特定のデータをサーバーに送りたかった

10行ほどで済んだのですが、しばらくJavaScript触ってなくて、地味に時間かかってまった・・・
Node.jsとExpressさまさまです。

```javascript:index.js
const express = require("express");

const app = express();
app.use(express.urlencoded({ extended: true }));

app.post('/', (req, res) => {
  const param = req.body.name
  console.log(param)
  res.send(`<h1>x-www-form-urlencodedを受け取る</h1><h1>${param}</h1>`);
});

// ポート3000番でlistenする
app.listen(3000);
```

## 参考

- [The Ultimate Guide to WKWebView](https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebvie)