【iOS】WKWebViewを閉じて外部ブラウザに遷移するJSIFを実装する
本記事は、『Swift Advent Calendar 2025』12日目の記事です。
はじめに
iOS SDKの開発で「WebViewを閉じて、指定されたURLを外部ブラウザで開く」
という機能を実装する機会がありました。
本記事では、JavaScript Interface(JSIF)を使ってこの機能を実装する手順を解説します。
対象読者
- WKWebViewを使ったアプリを開発している方
- Web↔ネイティブ連携に興味がある方
実現したいこと
外部サービスとの連携フローで、以下の動作が必要でした:
- WebView内でユーザーが操作
- 特定のタイミングでWebViewを閉じる
- 外部ブラウザで指定されたURLを開く
JSIF(JavaScript Interface)とは
Web側(JavaScript)からネイティブ(Swift)の機能を呼び出すための仕組みです。
WebViewとは?(前提)
まず、WebViewとは「アプリの中にWebページを表示する機能」です。
┌─────────────────────────────┐
│ ネイティブアプリ(Swift) │
│ │
│ ┌───────────────────────┐ │
│ │ WebView │ │
│ │ (Webページを表示) │ │
│ │ │ │
│ │ HTML/CSS/JavaScript │ │
│ │ で作られた画面 │ │
│ └───────────────────────┘ │
│ │
└─────────────────────────────┘
なぜJSIFが必要なのか?
WebView内のJavaScriptだけでは、以下のようなスマホ固有の機能にアクセスできません:
- 画面の明るさを変える
- 端末に安全にデータを保存する
- 他のアプリ(Safari等)を開く
- WebView自体を閉じる
そこで、JavaScriptから「この処理をやって!」とネイティブ側にお願いする仕組みが必要です。
これがJSIF(JavaScript Interface)です。
全体の流れ
Web(JavaScript)
↓ postMessage
WKScriptMessageHandler(メッセージ受信)
↓ パース
enum(メソッド判定)
↓
ViewController(実際の処理)
パースとは?
「パース」とは、データを解析してプログラムが扱いやすい形に変換することです。
Web側から送られてくるデータはJSON形式の文字列です。
これをSwiftの構造体に変換することで、簡単に値を取り出せるようになります。
| 状態 | データの形 |
|---|---|
| パース前 |
"{\"url\": \"https://example.com\"}" (文字列) |
| パース後 |
OpenExternalBrowserArgs(url: "https://example.com") (構造体) |
パース後は args.url のように簡単に値を取得できます。
実装手順
Step 1: 引数オブジェクトを定義する
Web側から送られてくるJSON文字列を、Swiftで扱いやすい形に変換(パース)するには、
変換先となる「型」が必要です。これが引数オブジェクト(構造体)です。
struct OpenExternalBrowserArgs: Codable {
/// 外部ブラウザで開くURL
let url: String
}
なぜ必要?
- JSONDecoder は「どんな形に変換するか」を知る必要がある
- 構造体を定義することで、変換先の形を指定できる
例えば、
json dataで
json = {
"name": "太郎"
"age": "20"
}
let name = json["name"]
と書かれているとき、swiftで構造体化することにより下記のように書くことができます。
struct User: Codable {
let name: String
let age: String
}
let user = User(name: "太郎", age: "20")
let name = user.name
ポイント:
-
Codableに準拠することでJSONから自動変換できる
参考記事:
Step 2: メソッドをenumに登録する
enumとは?
enum(列挙型)とは、関連する値をグループ化するための仕組みです。
例えば、信号機の色を表す場合:
enum Signal {
case red
case yellow
case green
}
これにより「信号機の色は赤・黄・緑のどれか」という制限ができます。
存在しない色(例:青)を指定するとコンパイルエラーになるため、
バグを防ぐことができます。
なぜJSIFでenumを使うのか?
JSIFのメソッド名を文字列で管理すると、タイプミスに気づけません:
// ❌ 文字列だとタイプミスに気づけない
if method == "opneExternalBrowser" { // スペルミス!
// 実行されない...
}
enumで管理すれば、コンパイラがチェックしてくれます:
// ✅ enumならコンパイルエラーで気づける
case .opneExternalBrowser: // エラー:そんなcaseはありません
JSIFのメソッド一覧を管理するenumに追加します
enum JavaScriptInterface {
// ... 既存のcase
case openExternalBrowser(OpenExternalBrowserArgs)
}
enum Method: String {
// ... 既存のcase
case openExternalBrowser
}
ここで学んだこと:
Swiftのenumには2種類あります:
| 番号 | 種類 | 特徴 |
|---|---|---|
| ① | Associated Value | 引数を持てる(例:case xxx(Args)) |
| ② | Raw Type (: String) |
文字列と対応、引数を持てない |
今回は両方を使い分ける必要がありました。
なぜ2種類必要?
// Web側から送られてくるのは「文字列」
let methodName = "openExternalBrowser" // 文字列
// 文字列 → enum に変換するために ②Raw Type が必要
let method = Method(rawValue: methodName) // Method.openExternalBrowser
// 処理には引数データが必要なので ①Associated Value も必要
case openExternalBrowser(OpenExternalBrowserArgs) // 引数を持てる
Step 3: パース処理を追加する
Web側から送られてきたJSONをパースします。
case .openExternalBrowser:
guard let paramsJson = object["params"] else {
handler(.failure(.paramsNotFound))
return
}
guard let args = try? JSONDecoder().decode(
OpenExternalBrowserArgs.self,
from: Data(paramsJson.utf8)
) else {
handler(.failure(.paramsParseError))
return
}
handler(.success(.openExternalBrowser(args)))
コード解説:
-
object["params"]でパラメータを取得-
objectはWeb側から送られてきたメッセージを辞書型に変換したものです - 元は
message.body as? [String: String]で型変換されている-
message.bodyは「何でも入る型(Any型)」 -
as?で辞書型に変換することで、object["params"]のようにアクセスできる
-
-
["params"]でキーを指定して値を取り出す
-
参考:
- Apple公式のWKScriptMessage(Web側からのメッセージを受け取る仕組み)
- 辞書型の値の取り方について
-
JSONDecoder().decode()でSwiftの構造体に変換 -
成功したら次の処理に渡す
Step 4: 実際の処理を実装する
WebViewを閉じて、外部ブラウザでURLを開きます。
func openExternalBrowser(_ args: OpenExternalBrowserArgs) {
// WebViewを閉じる
dismiss(animated: true)
// コールバックを呼ぶ
delegate?.onPageClosed(reason: "OPEN_BROWSER")
// 外部ブラウザでURLを開く
if let url = URL(string: args.url) {
UIApplication.shared.open(url)
}
}
コード解説:
-
dismiss(animated: true)でWebViewを閉じる- 現在表示中の画面を閉じるメソッド
-
delegate?.onPageClosed(reason: "OPEN_BROWSER")でコールバックを呼ぶ- SDKを使っているアプリに「WebViewが閉じたよ」と通知する仕組み
-
?はdelegateがnilの場合は何もしない(Optional Chaining)
-
if let url = URL(string: args.url)でURLを開く- 文字列をURL型に変換(失敗する可能性があるのでif letで安全に処理)
-
UIApplication.shared.open(url)で外部ブラウザを起動
参考:
- Apple公式: UIApplication.open
動作確認の方法
サンプルアプリにテスト用のボタンを追加して動作確認しました。
- サンプルアプリにJSIFを呼び出すボタンを追加
- シミュレータまたは実機でアプリを起動
- ボタンをタップしてJSIFを実行
- 以下を確認:
- WebViewが閉じるか
- 外部ブラウザが開くか
- 正しいURLが表示されるか
確認したログ
[JSIF_CALL] method: openExternalBrowser
[CALLBACK] onPageClosed, reason: OPEN_BROWSER
[DEBUG] WebViewController deinit
ログで以下が確認できれば成功です:
- JSIFが呼び出されている
- コールバックが実行されている
- WebViewControllerが解放されている(deinit)
まとめ
JSIFを追加する手順:
- 引数オブジェクトを定義(Codable)
- enumにcase追加
- パース処理を追加
- 実際の処理を実装
Web↔ネイティブ連携は複雑に見えますが、既存のコードパターンに従えば実装できることが分かりました。
Discussion