「The Ultimate Guide to WKWebView」をSwiftUIで実装してみるの、
5つ目になります。
この回から、WKNavigationDelegate
プロトコルを使っていきます。
なので、今まで実装していなかった、makeCoordinator
メソッドを使う必要があります。
あまり詳しくは説明しませんが、
UIKitで実装したViewからSwiftUIのViewにイベントを伝えたい時に使います。
目次
シリーズ化していこうと思うので、全体の目次を置いておきます。
リンクが貼られていないタイトルは、記事作成中または未作成のものになります。
# | タイトル |
---|---|
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
実現したいこと
今回やることは
WebView内にある特定のリンクの押下を検知して、アラートを表示させることです。
それ以外の場合は、普通に押下したリンク先のページが開くようにしています。
これができたもの。
実現方法
まずWebViewです。
今回は、画面のフッターに戻るボタンと先に進むボタンを追加してみたこと、
あと前述したように、WKNavigationDelegate
に準拠したメソッドを使用しているので、
コードが長めです。
特定のリンクの検知を行なっているのは、CoordinatorクラスにあるwebView(_:decidePolicyFor:decisionHandler:)
メソッドです。
http
またはhttps
がスキームになっているリンクがきた場合は、普通に開きますが、
sample-app
がスキームになっているリンクがきた場合、かつホストがalert
の場合は、アラート画面を表示するためのフラグをtrue
にします。
struct WebView: UIViewRepresentable {
let url: String
@ObservedObject var viewModel: WebViewModel
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// これ大事。委譲先を設定しないと、webView(_:decidePolicyFor:decisionHandler:)メソッドが実行されない
uiView.navigationDelegate = context.coordinator
if viewModel.needsGoBack {
uiView.goBack()
}
if viewModel.needsGoForward {
uiView.goForward()
}
guard let url = URL(string: url) else {
return
}
let request = URLRequest(url: url)
uiView.load(request)
}
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
}
extension WebView {
class Coordinator: NSObject, WKNavigationDelegate {
private let parent: WebView
private let viewModel: WebViewModel
init(_ parent: WebView, viewModel: WebViewModel) {
self.parent = parent
self.viewModel = viewModel
}
// decisionHandlerを実行しないケースがあると、クラッシュするので注意
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let scheme = navigationAction.request.url?.scheme else {
decisionHandler(.cancel)
return
}
switch scheme {
case "http", "https":
// httpまたはhttpsの場合は、表示
decisionHandler(.allow)
case "sample-app":
guard let host = navigationAction.request.url?.host else {
decisionHandler(.cancel)
return
}
// sample-app://alert の場合は、アラートを表示
if host == "alert" {
viewModel.isShownAlert = true
decisionHandler(.cancel)
}
default:
decisionHandler(.cancel)
}
}
}
}
WKWebView
初心者あるあるの過ちみたいですが
webView(_:decidePolicyFor:decisionHandler:)
メソッドは、decisionHandler
を実行しないケースがあるとアプリがクラッシュしてしまいます。
つまり例えばこのコードで、decisionHandler(.cancel)
を書き忘れると、
もしscheme
がnilになるようなリンクがやってきた場合に、クラッシュしてしまいます。
私は以前これで詰まった・・・
guard let scheme = navigationAction.request.url?.scheme else {
decisionHandler(.cancel)
return
}
ちなみに表示しているHTMLはこんな感じ。
<!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>
<h1>HTMLを表示するテスト</h1>
<a href="sample-app://alert">ネイティブアプリでアラートを表示</a> <br />
<a href="https://www.apple.com/">Appleのホームページを表示</a>
</body>
</html>
Webアプリとの行き来が頻繁に発生するようなアプリでは、ネイティブアプリ側にイベントやデータを渡すことはよくあるようです。
(あるようです。というのは、私の少ない経験上ではまだ1回しかないから...)
ちょっと話はそれますが、iOSアプリではディープリンクの方法は代表的な方法として、以下2つがあります。
- カスタムURLスキーム
- ユニバーサルリンク
今回の実装は、HTMLにあった特定のリンクの文字列をフックしただけで、カスタムURLスキームの実装とは言えない気がします。(でも一般的なディープリンクの定義には当てはまるのかな?)
ですが結構近いことはしていると思うので、
この記事とは別で、カスタムURLスキームやユニバーサルリンクについても調べてみたいと思っています。
次にWebViewを使っているViewです。
このViewに、フッターのボタンが定義されています。
import SwiftUI
struct WebBaseView: View {
let url: String
@ObservedObject var viewModel: WebViewModel
var body: some View {
VStack(spacing: 0) {
WebView(url: url, viewModel: viewModel)
footer
}
}
}
private extension WebBaseView {
var footer: some View {
HStack(alignment: .center) {
goBackButton
goForwardButton
Spacer()
}
.padding(.horizontal, 16)
.frame(maxWidth: .infinity, minHeight: 50)
.background(.yellow)
}
var goBackButton: some View {
Button(action: {
viewModel.goBack()
}) {
Image(systemName: "chevron.backward")
}
.frame(width: 30, height: 30)
}
var goForwardButton: some View {
Button(action: {
viewModel.goForward()
}) {
Image(systemName: "chevron.forward")
}
.frame(width: 30, height: 30)
}
}
そして最初のWebViewを見て気づいた方も多いと思いますが、今回はViewModelクラスを作っています。
needsGoBack
やneedsGoForward
がtrueになると、WebView.swift側でそれを検知して
前のページに戻る、先に進む、の処理が実行されます。
本題ではないので、最後の方にこのコードを持ってきてしまいましたが、
実装するときはもっと早い段階で作ることになるクラスだと思います。
import Foundation
class WebViewModel: ObservableObject {
@Published var needsGoBack = false
@Published var needsGoForward = false
@Published var isShownAlert = false
func goBack() {
needsGoBack = true
}
func goForward() {
needsGoForward = true
}
}
では最後に、WebBaseViewを使うViewになります。
ここに、アラート画面の実装があります。
import SwiftUI
struct ContentView: View {
private let url = "http://localhost:3000"
@ObservedObject private var viewModel = WebViewModel()
var body: some View {
WebBaseView(url: url, viewModel: viewModel)
.alert("URLスキームをフックした", isPresented: $viewModel.isShownAlert) {
Button(action: {}) {
Text("OK")
}
}
}
}
以上になります。
コード全体はこちらに上がっています。
今回元ネタはHacking with Swiftの「The Ultimate Guide to WKWebView」なのですが、
載っていたコードが個人的にあまり現実味がなかったので、もうちょっと実務で使われてそうな例に変更しました。
あと元の記事ではif let
でOptional型のアンラップをしていたのですが、
実際書いてみたら案の定ネスト地獄になったので、guard let
を使うようにしています。
その結果、原型留めないコードになりましたが、まあいいでしょう。
誰かの役に立ったら嬉しいです。