LoginSignup
3
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-17

「The Ultimate Guide to WKWebView」をSwiftUIで実装してみるの、
12こ目になります。

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

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

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

目次

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

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

環境

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

実現したいこと

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

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

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

app.gif

実現方法

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

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

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

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

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プロパティに値を設定するのみになります。

ドキュメントのディスカッションにも書いている通り👇、デフォルト値が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は、こちらのMDN Web Docsによると

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

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

ちなみにRFC7131に記載されている通り、

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

なのだそうです。

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


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

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) {
    }
}

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

デフォルトのUser Agentの値は残しつつ、SuffixにカスタムUser Agentの値を加えたい場合は、
WKWebViewConfigurationクラスのapplicationNameForUserAgentプロパティを使って設定します。

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

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

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

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

どれも興味ありますが今回触れるのは、applicationNameForUserAgentプロパティです。

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

The app name that appears in the user agent string.

この設定をした上で、WKWebViewの初期化メソッドinit(frame:configuration:)の第二引数に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の大きさを指定しているという認識です。。!


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

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)
        }
    }
}

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

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

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

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

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

以上です!

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

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

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

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

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

その他調べたこと

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

APIの用意

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

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

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

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);

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

参考

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1