「The Ultimate Guide to WKWebView」をSwiftUIで実装する #12 - Providing a custom user agent -

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

今回はUser-Agentの設定についてです！
元記事に書いていた設定方法以外も試しています

そもそも開発者が明示的にUser Agentを指定するというケースがあるのか、というのが疑問ではあったのですが、そうせざるを得ない場合もあると思います。私はあった・・・

そういう方のお役に立てば幸いです。

## 目次

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

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

## 実現したいこと

特定の文字列を含んだUser-Agentからのリクエストの場合のみ、200 OKを返します。

カスタムUser-Agentのありのパターンを２つ、なしのパターンを１つ
で実装しています。

サーバー側で取得したUser-AgentをHTMLに埋め込んでいるので
どんなUser-Agentがサーバーにやってきたかわかりやすいかと思います。

![app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/e4141743-d8e9-4c79-e467-d81da1af8680.gif)

## 実現方法

今回は今までの中ではコードも短めです。
なので何パターンか実装しています。

上述したのですが、改めて詳細を書くとこの３パターンです。

1. **デフォルトのUser Agentの値を上書きして、独自のUser Agentを付与**する
1. **デフォルトのUser Agentの値をそのまま使う**→そのために開発者がコードを書く必要はなし
1. **デフォルトのUser Agentの値に加えて、独自のUser AgentをSuffixで付与**する

ではまず1つ目と2つ目のパターンのWebViewです。

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

struct WebView: UIViewRepresentable {
    let url: URL
    @Binding var hasUserAgent: Bool
    private let webView = WKWebView()

    func makeUIView(context: Context) -> WKWebView {
        let request = URLRequest(url: url)
        if hasUserAgent {
　　　　　　　　　　　　　　　　　　　　　　　　// カスタムUser Agentの付与
            webView.customUserAgent = "ProvidingACustomUserAgent"
        }
        webView.load(request)
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
    }
}
```

見ての通りですが、カスタムUser Agentを付与するために書くコードは1行のみです。

[`customUserAgent`](https://developer.apple.com/documentation/webkit/wkwebview/1414950-customuseragent)プロパティに値を設定するのみになります。

ドキュメントのディスカッションにも書いている通り👇、デフォルト値がnilなのでカスタムUser Agentを設定したいという場合以外では設定しなくてOKです。

> Use this property to specify a custom user agent string for your web view. The default value of this property is nil.

このプロパティを用いてUser Agentを設定すると、User-Agentの値は以下の通りになります。

```
'user-agent': 'ProvidingACustomUserAgent'
```

これを設定しないと、User-Agentの値はこうなります。

```
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'
```

つまり例え開発者が明示的にUser Agentを指定しなくても、勝手に設定してくれるデフォルト値があるわけです。

そもそもUser-Agentは、[こちら](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/User-Agent)のMDN Web Docsによると

**「サーバーやネットワークピアがアプリケーション、オペレーティングシステム、ベンダーや、リクエストしているユーザーエージェントのバージョン等を識別できるようにする特性文字列」**

なので、そうなっているのかなと推測します。

ちなみに[RFC7131](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.3)に記載されている通り、

**「特別そう設定されていない限り、各リクエストにUser-Agentフィールドを付与して送るべき」**

なのだそうです。

今回このUser-AgentについてはWeb側の動向も含めて調べなおしたので、気になる方は以下も一緒にご参照ください。:pray:

https://zenn.dev/kamimi01/scraps/2be3eee6bc2f65

---

次に３つ目のパターンです。

```swift:WebViewWithSuffixCustomUA.swift
import WebKit
import SwiftUI

struct WebViewWithSuffixCustomUA: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        let request = URLRequest(url: url)

        // カスタムUser AgentをSuffixに設定
        let config = WKWebViewConfiguration()
        config.applicationNameForUserAgent = "ProvidingACustomUserAgent"
        let webView = WKWebView(frame: .zero, configuration: config)

        webView.load(request)
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
    }
}
```

こちらは先ほどと比べて少しコード量が多いです。といっても３行になったのみです。

デフォルトのUser Agentの値は残しつつ、SuffixにカスタムUser Agentの値を加えたい場合は、
[`WKWebViewConfiguration`](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration)クラスの`applicationNameForUserAgent`プロパティを使って設定します。

この`WKWebViewConfiguration`クラスですが、名前からも想定つく通り、User Agentの設定のみに使用するクラスではありません。

WKWebViewを初期化するとき様々な設定をすることができるのですが、その設定のために使用されます。

設定できるものには例えば以下のようなものがあり、その中の１つがカスタムUser Agentの付与です。

・Webコンテンツで利用できるようにするための、初期cookie
・Webコンテンツが使用するカスタムURLスキームのハンドラー
・Mediaコンテンツを操作するための設定
・WebView内での選択の管理方法の情報
・Webページに注入するカスタムスクリプト
・コンテンツの描画方法を決めるためのカスタムルール

どれも興味ありますが今回触れるのは、[`applicationNameForUserAgent`](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1395665-applicationnameforuseragent)プロパティです。

ドキュメントの説明はこれだけ👇で、どこにもSuffixに追加されることは書いてないのですが、Suffixに追加されるようです。

> The app name that appears in the user agent string.

この設定をした上で、WKWebViewの初期化メソッド[`init(frame:configuration:)`](https://developer.apple.com/documentation/webkit/wkwebview/1414998-init)の第二引数に`WKWebViewConfiguration`を渡します。

実際にサーバー側で受け取ったUser Agent文字列を見てみます。
右にスクロールしていくと、一番最後に`ProvidingACustomUserAgent`の文字列が追加されていることがわかります。

```
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ProvidingACustomUserAgent'
```

ちなみに第一引数の`frame`が気になると思うのですが、これはCGRect型をとり、今回は`.zero`を指定しています。

「新しいWebViewのためのフレームの四角形」なのだとか。

> The frame rectangle for the new web view.

今回は触れませんが、表示するWebViewの大きさを指定しているという認識です。。！

---

では最後に、アプリの初期表示画面です。

```swift:ContentView.Swift
import SwiftUI

struct ContentView: View {
    @State private var isShownWebView = false
    @State private var isShownWebViewWithSuffixCustomUA = false
    @State private var hasUserAgent = false
    private let url = URL(string: "http://localhost:3000/")!

    var body: some View {
        VStack {
            Button(action: {
                hasUserAgent = true
                isShownWebView.toggle()
            }) {
                Text("カスタムUAあり")
            }
            Button(action: {
                hasUserAgent = false
                isShownWebView.toggle()
            }) {
                Text("カスタムUAなし")
            }
            Button(action: {
                isShownWebViewWithSuffixCustomUA.toggle()
            }) {
                Text("SuffixにカスタムUAあり")
            }
        }
        .sheet(isPresented: $isShownWebView) {
            WebView(url: url, hasUserAgent: $hasUserAgent)
        }
        .sheet(isPresented: $isShownWebViewWithSuffixCustomUA) {
            WebViewWithSuffixCustomUA(url: url)
        }
    }
}
```

前述した３パターンを再現するための、ボタンが３つ配置されています。

今回の実装で、１つのViewに対して複数のsheetモディファイアを適用することが可能になったんだ！というのは発見でした。

**iOS14.5から可能になった**らしいです。

それ以前は、ボタンごとにsheetモディファイアを適用したり、使用するモディファイアは１つのみにしてその中でswitchやif文で条件分岐したりとか頑張ってたのですが、
それが不要になるのは嬉しいです。

もちろんiOS14.5より下のバージョンをサポート対象外にできないのであれば、この書き方👆をすべきではないと思います。:cry:

以上です！

コード全体は以下に上がっています。

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

今回は改めてUser-Agentなんだっけ、というのを調べるいい機会になりました。

最初にも書いた通り、そもそもUser Agent文字列を開発者が明示的に指定するというケース自体は多くないと思っていますが、私は実務でそういうAPIに出会ったので、今回詳しく調査してみました。:blush:

[参考](#参考)のリンクには、実際の世の中で使われているアプリのUser Agentを覗いてみたという記事もあるので、それも参考になるかと思いますが、明示的に指定しているアプリはあまりないようです。

また現時点ではchromeブラウザからのリクエストで盛り上がっている話題ですが、User Agent Client Hintsの動向も気になります。気になる方は、[こちら](https://zenn.dev/link/comments/add2109871f339)に貼ったリンクをご参照ください。

## その他調べたこと

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

### APIの用意

最初に貼ったGIF画像を見れば、どんなAPIを実装したのかわかるかとは思うのですが、改めて。

リクエストヘッダの`User-Agent`フィールドに`ProvidingACustomUserAgent`の文字列が含まれている場合は、200 OKを返します。
含まれていない場合は、400 Bad Request を返すようにしています。

両方に共通しているのは、サーバー側でどんなUser Agent文字列を受け取ったのかがわかりやすいように、
HTMLに受け取った文字列を埋め込んで返していることですね。

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

app.get('/', (req, res) => {
  const userAgent = req.headers["user-agent"];
  if (userAgent.includes('ProvidingACustomUserAgent')) {
    const htmlString = `<h1 style="font-size: 50px;">200 OK</h1> \
    <br><h1 style="font-size: 50px;">${userAgent}</h1>`;
    res.send(htmlString);  
    return
  }

  const htmlString = `<h1 style="font-size: 50px;">400 Bad Request</h1> \
  <br><h1 style="font-size: 50px;">${userAgent}</h1>`;
  res.status(400);
  res.send(htmlString);  
});

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

誰かの役に立てば幸いです。

## 参考

- [`User-Agent` MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/User-Agent)
- [RFC7231 5.5.3 User-Agent](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.3)
- [モバイルアプリのユーザーエージェント設計のベストプラクティス](https://dev.classmethod.jp/articles/user-agent-best-practice/)
- [Charlesを使ってスマホアプリ(iOS)のUAを調べてみた](http://yebisupress.dac.co.jp/2015/09/01/charles%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%82%B9%E3%83%9E%E3%83%9B%E3%82%A2%E3%83%97%E3%83%AAios%E3%81%AEua%E3%82%92%E8%AA%BF%E3%81%B9%E3%81%A6%E3%81%BF%E3%81%9F/)
- [2021年版 主要なアプリ内ブラウザのUserAgentを調べてみた & 判定を実装してみた](https://zenn.dev/kecy/articles/f51851e42c4243)
- [WKWebViewのカスタムUserAgentを設定する方法](https://sussan-po.com/2019/11/22/wkwebview-user-agent/)
- [`customUserAgent` Apple公式ドキュメント](https://developer.apple.com/documentation/webkit/wkwebview/1414950-customuseragent)
- [`WKWebViewConfiguration` Apple公式ドキュメント](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration)
- [`applicationNameForUserAgent` Apple公式ドキュメント](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1395665-applicationnameforuseragent)
- [`init(frame:configuration:)` Apple公式ドキュメント](https://developer.apple.com/documentation/webkit/wkwebview/1414998-init)
- [【Swift】CGRectの使い方まとめ](https://naoya-ono.com/swift/cgrect/)
- [`zero` Apple公式ドキュメント](https://developer.apple.com/documentation/coregraphics/cgrect/1455437-zero)
- [iOS14.5でSwiftUIがmultiple sheetに対応したので確認した](https://zenn.dev/yorifuji/articles/swiftui-multiple-sheet)