「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がサーバーにやってきたかわかりやすいかと思います。
実現方法
今回は今までの中ではコードも短めです。
なので何パターンか実装しています。
上述したのですが、改めて詳細を書くとこの3パターンです。
- デフォルトのUser Agentの値を上書きして、独自のUser Agentを付与する
- デフォルトのUser Agentの値をそのまま使う→そのために開発者がコードを書く必要はなし
- デフォルトのUser Agentの値に加えて、独自のUser AgentをSuffixで付与する
ではまず1つ目と2つ目のパターンのWebViewです。
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側の動向も含めて調べなおしたので、気になる方は以下も一緒にご参照ください。
次に3つ目のパターンです。
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の大きさを指定しているという認識です。。!
では最後に、アプリの初期表示画面です。
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より下のバージョンをサポート対象外にできないのであれば、この書き方👆をすべきではないと思います。
以上です!
コード全体は以下に上がっています。
今回は改めてUser-Agentなんだっけ、というのを調べるいい機会になりました。
最初にも書いた通り、そもそもUser Agent文字列を開発者が明示的に指定するというケース自体は多くないと思っていますが、私は実務でそういうAPIに出会ったので、今回詳しく調査してみました。
参考のリンクには、実際の世の中で使われているアプリのUser Agentを覗いてみたという記事もあるので、それも参考になるかと思いますが、明示的に指定しているアプリはあまりないようです。
また現時点ではchromeブラウザからのリクエストで盛り上がっている話題ですが、User Agent Client Hintsの動向も気になります。気になる方は、こちらに貼ったリンクをご参照ください。
その他調べたこと
WebViewやSwiftUI自体とは関係ないですが、調べたこと、やったことを書いておきます。
APIの用意
最初に貼ったGIF画像を見れば、どんなAPIを実装したのかわかるかとは思うのですが、改めて。
リクエストヘッダのUser-Agent
フィールドにProvidingACustomUserAgent
の文字列が含まれている場合は、200 OKを返します。
含まれていない場合は、400 Bad Request を返すようにしています。
両方に共通しているのは、サーバー側でどんなUser Agent文字列を受け取ったのかがわかりやすいように、
HTMLに受け取った文字列を埋め込んで返していることですね。
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- RFC7231 5.5.3 User-Agent
- モバイルアプリのユーザーエージェント設計のベストプラクティス
- Charlesを使ってスマホアプリ(iOS)のUAを調べてみた
- 2021年版 主要なアプリ内ブラウザのUserAgentを調べてみた & 判定を実装してみた
- WKWebViewのカスタムUserAgentを設定する方法
customUserAgent
Apple公式ドキュメントWKWebViewConfiguration
Apple公式ドキュメントapplicationNameForUserAgent
Apple公式ドキュメントinit(frame:configuration:)
Apple公式ドキュメント- 【Swift】CGRectの使い方まとめ
zero
Apple公式ドキュメント- iOS14.5でSwiftUIがmultiple sheetに対応したので確認した