はじめに
iOS14時点でのSwiftUIでは現在、WKWebViewに相当するViewのサポートを行っておりません。
そのため、UIKitのWKWebViewをSwiftUI用にラップして使用することが必要となります。
今回は基本的な機能(ツールバー、プログレスバー)を搭載したWebViewを表示させることをゴールとし、その中で出てくるUIViewRepresentable
の使い方についてまとめていきたいと思います。
※ 本記事では汎用性を考え、iOS13まででサポートされている技術を使用してWebViewの表示を行います。
UIViewRepresentableについて
UIKitのViewをSwiftUIで使用するにはUIViewRepresentable
を使用する必要があります。
UIViewRepresentable
とはSwiftUIにてUIKitのViewを使用するためのラッパーです。
UIViewRepresentable
のプロトコルで定義されている各関数について説明します。
func makeUIView(context: Self.Context) -> Self.UIViewType
実装必須。
表示するViewのインスタンスを生成します。
SwiftUIにて使用したUIKitのViewを戻り値として返却します。
func updateUIView(Self.UIViewType, context: Self.Context)
実装必須。
アプリの状態が更新される場合に呼ばれます。
Viewの更新がある場合は、本関数の中に記述します。
static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)
指定したUIKitのViewが削除される際に呼び出されます。
必要に応じて登録した通知の削除など、クリーンアップ処理を本関数内に記述します。
func makeCoordinator() -> Self.Coordinator
View側から通知すべきイベントがある場合に実装します。
Coordinator
を定義することで、Delegateのようなユーザの操作によるイベントハンドリングを行うことができるようになります。
WKWebViewを使ってWebViewを作る
では本題のWKWebViewの表示に入っていきます。
WKWebViewを表示する
まずは単純にWebViewをSwiftUIで表示する方法です。
UIViewType
はassociatedtype
になりますので、ここをラップしたいUIKitのViewの型に変更します。
今回はWKWebViewにします。
struct WebView: UIViewRepresentable {
/// 表示するView
private let webView = WKWebView()
/// 表示するURL
let url: URL
func makeUIView(context: Context) -> WKWebView {
// 戻り値をWKWebViewとし、返却する
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
ツールバーを作成する
次に戻る
進む
リロード
の3つの要素を持ったツールバーを作成していきます。
ボタンタップ時のアクションの制御
まずはツールバーを作成し、各ボタンを配置します。
次にProperty Wrappers
を使用して、ツールバーにて各ボタンがタップされた際にWebView側で検知できるようにします。
この状態でツールバーのボタンをタップすると、アプリの状態を更新され、UIViewRepresentable
のupdateUIView(_ uiView:, context:)
が発火します。
上記から、WebView側ではupdateUIView(_ uiView:, context:)
の中にボタンに応じたアクションを記述します。
struct WebView: UIViewRepresentable {
// 省略...
/// WebViewのアクション
enum Action {
case none
case goBack
case goForward
case reload
}
/// アクション
@Binding var action: Action
func updateUIView(_ uiView: WKWebView, context: Context) {
/// バインドしている値が更新されるたびに呼ばれる
/// actionが更新されたら、更新された値に応じて処理を行う
switch action {
case .goBack:
uiView.goBack()
case .goForward:
uiView.goForward()
case .reload:
uiView.reload()
case .none:
break
}
action = .none
}
}
struct WebToolBarView: View {
/// アクション
@Binding var action: WebView.Action
var body: some View {
VStack() {
HStack() {
// タップしたボタンに応じてアクションを更新
Button("Back") { action = .goBack }
Button("Forward") { action = .goForward }
Button("Reload") { action = .reload }
}
}
}
}
struct RichWebView: View {
/// URL
let url: URL
/// アクション
@State private var action: WebView.Action = .none
var body: some View {
VStack() {
WebView(url: url,
action: $action)
WebToolBarView(action: $action)
}
}
}
※ @State
や@Binding
などのProperty Wrappers
については、本題から逸れるためここでは解説しません。
詳しく解説されている記事が多く存在しますので、必要に応じて別途ご参照下さい。
State and Data Flow | Apple Developer Documentation
ボタンの非活性化
これで、WebViewでボタンタップ時に処理をさせることが可能になりました。
しかし、どんな状態でもボタンがタップできてしまいますので、前後のページに移動できない場合は各ボタンを非活性化させるようにします。
Coordinatorを定義する
前後のページへ移動できるかどうかをWebViewのページ読み込みが完了したタイミングで判断するようにします。
そのためにはWKNavigationDelegate
を実装する必要がありますが、直接WebViewに実装することはできません。
そこでUIViewRepresentable
では、Coordinator
というUIKitのViewから受け取った変更をSwiftUIに伝えるためのカスタムインスタンスを作成する必要があります。
WKNavigationDelegate
はCoordinator
に実装し、それを通してSwiftUI側のイベントハンドリングを行います。
struct WebView: UIViewRepresentable {
// 省略...
/// 戻れるか
@Binding var canGoBack: Bool
/// 進めるか
@Binding var canGoForward: Bool
func makeCoordinator() -> WebView.Coordinator {
return Coordinator(parent: self)
}
}
extension WebView {
final class Coordinator: NSObject, WKNavigationDelegate {
/// 親View
let parent: WebView
init(parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.canGoBack = webView.canGoBack
parent.canGoForward = webView.canGoForward
}
}
}
あとはRichWebView
を介してWebToolBarView
にて各値を受け取り、各ボタンの非活性、活性の処理を記述すれば完了です。
Button("Back") { action = .goBack }.disabled(!canGoBack)
Button("Forward") { action = .goForward }.disabled(!canGoForward)
プログレスバーを作成する
最後にプログレスバーを作成しましょう。
KVOで値を取ってくる
プログレスバーを作成するためにWKWebView
のestimatedProgress
とisLoading
を取得する必要があります。
今回はそれぞれKVOを使用して取ってきます。
Viewからの変更通知を受け取ることになりますので、先ほども使用したCoordinator
を使用します。
struct WebView: UIViewRepresentable {
// 省略...
/// 読み込みの進捗状況
@Binding var estimatedProgress: Double
/// ローディング中かどうか
@Binding var isLoading: Bool
static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
// WKWebView削除時に呼ばれます
// インスタンスが削除されるタイミングで通知を無効化、削除しておきます
coordinator.observations.forEach({ $0.invalidate() })
coordinator.observations.removeAll()
}
}
extension WebView {
final class Coordinator: NSObject, WKNavigationDelegate {
/// 親View
let parent: WebView
/// NSKeyValueObservations
var observations: [NSKeyValueObservation] = []
init(parent: WebView) {
self.parent = parent
// 通知を登録する
let progressObservation = parent.webView.observe(\.estimatedProgress, options: .new, changeHandler: { _, value in
parent.estimatedProgress = value.newValue ?? 0
})
let isLoadingObservation = parent.webView.observe(\.isLoading, options: .new, changeHandler: { _, value in
parent.isLoading = value.newValue ?? false
})
observations = [
progressObservation,
isLoadingObservation
]
}
// 省略...
}
}
```
#### プログレスバーを作成する
これでプログレスバーに必要なパーツは揃いました。
あとは`ProgressBarView.swift`を作成し、`RichWebView`を介して値を受け取って表示すれば完了です。
```swift:ProgressBarView.swift
struct ProgressBarView: View {
/// 読み込みの進捗状況
var estimatedProgress: Double
var body: some View {
VStack {
GeometryReader { geometry in
Rectangle()
.foregroundColor(Color.gray)
.opacity(0.3)
.frame(width: geometry.size.width)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: geometry.size.width * CGFloat(estimatedProgress))
}
}.frame(height: 3.0)
}
}
struct RichWebView: View {
// 省略...
/// 読み込みの進捗状況
@State private var estimatedProgress: Double = 0.0
/// ローディング中かどうか
@State private var isLoading: Bool = false
var body: some View {
VStack() {
if isLoading {
ProgressBarView(estimatedProgress: estimatedProgress)
}
// 省略...
}
}
}
これで一通りの機能を実装することができました。
エラーハンドリングなど別途考慮する点はありますが、一旦ざっくりとした機能を持ったWebViewを作成することができたのではないでしょうか。
おわりに
今回、リッチなWebViewの表示を目指して実装を行いましたが、SwiftUIでWebViewを実装するためにはUIViewRepresentable
の基本的な機能を使用して作成する必要があるため、勉強にちょうど良いと思います。
興味ある方はぜひやってみてください。
ソースに関しては下記のgithubにまとめておきますので興味がある方がいらっしゃいましたら見ていただけると幸いです。
(github側はナビバーのタイトル表示や、レイアウトのための制約などここでは省いたコードがいくつか入っています。)
RichWebViewSample
また、今回の実装について改善案やご意見等ありましたらコメントいただけますと幸いです。
どうぞよろしくお願いいたします。