「The Ultimate Guide to WKWebView」をSwiftUIで実装してみるの、
10こ目になります。
「#02」の記事以来の、JavaScriptとのご対面です。
目次
シリーズ化していこうと思うので、全体の目次を置いておきます。
リンクが貼られていないアジェンダは、記事作成中または未作成のものになります。
# | タイトル |
---|---|
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
実現したいこと
今回やることは、
まずiOSアプリ側で値を入力し、WebViewを開くときにその値を表示するHTMLに注入します。
とこれだけ書いてGIF画像を見ると、#02と変わらないように見えるのですが、
実現方法が以前と違います。
「#02」の時は、サーバーに値を送信し、サーバー側でHTMLに値を注入していたのですが
今回はサーバーには送らず、ネイティブ側でJavaScriptの関数を実行し、HTMLに値を注入します。
以下が実際の動きです。
また参考にもリンクを記載していますが、
今回のアプリの挙動は、「WKWebView and JavaScript interaction」を参考にしております。感謝。
実務で使ったことなかったから、どういうのがよくある使用例なのか調べるのに時間かかった・・・
まあこれも実際よく使われているのかは私にはわからないわけですが、そこは将来の自分がなんとかしてくれるはず。笑
実現方法
まずWebViewです。
少し長いのですが、メインはwebView(_:didFinish:)
メソッドにあるevaluateJavaScript(_:completionHandler:)
です。
Webページの読み込みが終了したら、JavaScriptを実行しています。
その下にあるcreateJsonForJavaScript
メソッドは、JavaScript実行時に必要となるJSON文字列を作成しています。
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
let name: String
let email: String
private let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
webView.navigationDelegate = context.coordinator
let request = URLRequest(url: url)
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
}
extension WebView {
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let parameters = ["name": parent.name, "email": parent.email]
guard let jsonString = createJsonForJavaScript(for: parameters) else {
return
}
let jsString = "fillDetails('\(jsonString)')"
// JavaScriptのメソッドを実行する
parent.webView.evaluateJavaScript(jsString) { value, error in
print(value) // 「executed javascript method」が表示される
print(error?.localizedDescription) // 成功した場合は、nilになる
}
}
// { \"email\" : \"mailmail\", \"name\" : \"Mika\"} のようなJSON文字列を生成する
private func createJsonForJavaScript(for data: [String: Any]) -> String? {
var jsonString: String?
do {
let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
jsonString = String(data: jsonData, encoding: .utf8)
jsonString = jsonString?.replacingOccurrences(of: "\n", with: "")
} catch {
print(error.localizedDescription)
}
print(jsonString)
return jsonString
}
}
}
もう少し詳しくみていきます。
まず、evaluateJavaScript(_:completionHandler:)
についてです。
これは、名前の通り、JavaScriptを評価するためのメソッドです。
引数は、以下の2つあります。
javaScriptString
completionHandler
まず第一引数javaScriptString
は、わかりやすいですね。実行したいJavaScript文字列です。
もし実行したいJavaScriptが長い場合は、別ファイルとしてアプリバンドル内に配置して、
こんな感じに↓読み込んでくる方法もあります。(コードは「WKWebViewを使ってNativeとWebページで情報をやり取りする方法」から引用させていただきました。)
ですが今回はその方法は使っていません。
var jsSource = ""
// JavaScriptコードはtest.jsという名前で別ファイルに用意
let path = NSBundle.mainBundle().pathForResource("test", ofType: "js")!
if let data = NSData(contentsOfFile: path){
jsSource = String(NSString(data: data, encoding: NSUTF8StringEncoding)!)
}
次にcompletionHandler
です。
公式ドキュメントによると、
「JavaScriptの評価が終わったら、実行されるブロック。メソッドはJavaScriptの評価の成功・失敗に関わらず、ブロックを呼び出す。ブロックは戻り値を持たず、次のパラメータを受け取る。
object
:JavaScriptの評価の結果。エラーが発生した場合はnil
error
:成功した場合は、nil。または問題の情報を持ったエラーオブジェクト。
」
A handler block to execute when script evaluation finishes. The method calls your block whether script evaluation completes successfully or fails. The block has no return value and takes the following parameters:
object
The result of the script evaluation, or nil if an error occurred.
error
nil on success, or an error object with information about the problem.
とのこと。
今回は、こんなJavaScriptのメソッドを用意してこれをネイティブ側から実行しているので、問題なくJavaScriptの実行が成功すれば
object
としては、戻り値であるexecuted javascript method
の文字列を受け取り、
error
はnil
となっています。
function fillDetails(jsonData) {
console.log("jsonData: ", jsonData);
let userData = JSON.parse(jsonData);
document.getElementById("name").value = userData["name"];
document.getElementById("email").value = userData["email"];
return "executed javascript method"
}
ちなみにHTMLはこうです。
テキストフィールドが2つ並んでいるだけのシンプルなページです。
<!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>
<input type="text" id="name">
<br>
<input type="text" id="email">
<script type="text/javascript" src="http://localhost:3000/js"></script>
</body>
</html>
また補足ですが、
JavaScriptを実行できるメソッドは、evaluateJavaScript(_:completionHandler:)
以外にもあるようです。
それがWKUserScript
クラスのメソッド。
evaluateJavaScript(_:completionHandler:)
と違い、どのようにJavaScriptを実行するのか、どのタイミングで実行するのかを制御できるそうです。
今回は取り上げません・・・
では最後に、WebViewを使用する初期表示画面のViewです。
テキストフィールドが2つとWebViewを表示するためのボタンが1つ置いてあります。
import SwiftUI
struct ContentView: View {
private let url = URL(string: "http://localhost:3000/")!
@State private var isShownWebView = false
@State private var name = ""
@State private var email = ""
var body: some View {
VStack {
TextField("name", text: $name)
.textInputAutocapitalization(.never)
.frame(width: 200)
.border(.yellow)
TextField("email", text: $email)
.textInputAutocapitalization(.never)
.frame(width: 200)
.border(.yellow)
Button(action: {
isShownWebView.toggle()
}) {
Text("値を送信してWebViewを開く")
}
}
.fullScreenCover(isPresented: $isShownWebView) {
WebView(url: url, name: name, email: email)
}
}
}
テキストフィールドに入力したとき、デフォルトだとアルファベットの最初の文字が大文字になってしまうので、
それを防ぐために、textInputAutocapitalization(_:)
を使用しています。
iOS15より前では、autocapitalization(_:)
だったのですが、Deprecatedになっていますね。iOS15より前をサポートしないなら、textInputAutocapitalization(_:)
を使った方が良いようです。
iOSの実装としては以上です!
「The Ultimate Guide to WKWebView」にはないですが、WKUserScript
も触ってみたいです。
コード全体は以下にあがっています。
その他調べたこと
WebViewやSwiftUI自体とは関係ないですが、調べたこと、やったことを書いておきます。
APIの用意
大したことはしていないのですが、、
index2.htmlを、http://localhost:3000/
で
index2.jsを、http://localhost:3000/js
で
呼び出せるようにしています。
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index2.html')
});
app.get('/js', (req, res) => {
res.sendFile(__dirname + '/index2.js')
});
// ポート3000番でlistenする
app.listen(3000);