現時点(2020/05/03)では SwiftUI に WebView は組み込まれていません。その為、UIViewRepresentable を使って WKWebView をラップすることになります。
SwiftUI で WebView を表示する
WebViewStateModel
先ず、WebView の状態を管理する ViewModel 的な役割の WebViewStateModel を作成します。
View 側で @ObservedObject として保持するため ObservableObject プロトコルを継承しています。
また、イニシャライザーでは URL の文字列を受け取ります。
class WebViewStateModel: ObservableObject {
@Published var isLoading = false
@Published var canGoBack = false
@Published var shouldGoBack = false
@Published var shouldLoad = false
@Published var title = ""
struct Error {
let code: URLError.Code
let message: String
}
@Published var error: Error?
private(set) var url: String
init(url: String) {
self.url = url
}
func load(_ url: String) {
self.url = url
shouldLoad = true
}
}
プロパティ
WebView の状態を取得するプロパティは以下の通りです。
- isLoading:読み込み中の場合 true になります
- canGoBack:true の場合、前のページに戻れる状態を指します
- shouldGoBack:true になると前の画面へ戻ります
- shouldLoad:true になると設定された URL の画面へ遷移します
- title:Webページのタイトルタグが格納されます
- error:読み込みエラーのコードとメッセージが格納されます
func load(_ url: String)
新しい画面へ遷移したい場合に呼び出します。
WebViewContainer
本題の UIViewRepresentable を継承した View を作成します。
struct WebViewContainer: UIViewRepresentable {}
プロパティとして、先ほど作成した WebViewStateModel を保持しています。
@ObservedObject var stateModel: WebViewStateModel
Coordinator
続いて、WKWebView をハンドリングするための内部クラス Coordinator を定義します。
class Coordinator: NSObject, WKNavigationDelegate {
@ObservedObject private var stateModel: WebViewStateModel
private let parent: WebViewContainer
init(parent: WebViewContainer, stateModel: WebViewStateModel) {
self.parent = parent
self.stateModel = stateModel
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
stateModel.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
stateModel.isLoading = false
stateModel.title = webView.title ?? ""
stateModel.canGoBack = webView.canGoBack
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
stateModel.isLoading = false
setError(error)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
stateModel.isLoading = false
setError(error)
}
private func setError(_ error: Error) {
if let error = error as? URLError {
stateModel.error = WebViewStateModel.Error(code: error.code, message: error.localizedDescription)
}
}
}
WebView のハンドリングするため、 WKNavigationDelegate に準拠し以下のメソッドを実装しています。
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)
- didStartProvisionalNavigation:読み込み開始をナビゲートします
- didFinish:読み込み完了をナビゲートします
- didFail:読み込み処理中のエラーをナビゲートします
- didFailProvisionalNavigation:読み込み開始時に発生したエラーをナビゲートします
UIViewRepresentable
View として利用するには UIViewRepresentable に準拠する必要があるため、以下の3つのメソッドを実装します。
func makeCoordinator() -> Coordinator
func makeUIView(context: Context) -> WKWebView
func updateUIView(_ uiView: WKWebView, context: Context)
makeCoordinator
先ほど紹介した内部クラス Coordinator を返すように実装します。
func makeCoordinator() -> Coordinator {
Coordinator(parent: self, stateModel: stateModel)
}
makeUIView
WKWebView を生成し、指定 URL のWebページを読み込みます。
func makeUIView(context: Context) -> WKWebView {
guard let url = URL(string: stateModel.url) else {
return WKWebView()
}
let webView = WKWebView()
let request = URLRequest(url: url)
webView.navigationDelegate = context.coordinator
webView.load(request)
return webView
}
updateUIView
shouldGoBack をチェックしブラウザバックを、または shouldLoad をチェックし設定されたURLのページへ遷移します。
func updateUIView(_ uiView: WKWebView, context: Context) {
if stateModel.shouldGoBack {
uiView.goBack()
stateModel.shouldGoBack = false
}
if stateModel.shouldLoad {
guard let url = URL(string: stateModel.url) else {
return
}
let request = URLRequest(url: url)
uiView.load(request)
stateModel.shouldLoad = false
}
}
使用例
初期画面で Apple の公式ページが表示されます。
また、ナビゲーションバーにブラウザバックボタンと Google のトップページが開くボタンを設置しています。
struct ContentView: View {
@ObservedObject var stateModel = WebViewStateModel(url: "https://apple.com")
var body: some View {
NavigationView {
ZStack {
WebViewContainer(stateModel: stateModel)
if stateModel.isLoading {
ProgressView()
}
}
.navigationBarTitle(Text(stateModel.title), displayMode: .inline)
.navigationBarItems(
leading: Button(action: {
stateModel.shouldGoBack = true
}) {
if stateModel.canGoBack {
Text("<Back")
} else {
EmptyView()
}
},
trailing: Button(action: {
stateModel.load("https://google.com")
}) {
Text("Google")
}
)
}
}
}
以上、WebView を SwiftUI で使用する一例を紹介しました。
早く SwiftUI 標準で搭載されるといいですね。
手順が端折りすぎていて全く理解出来ないです、、、特にWebViewContainer
ピエールさん
コメントありがとうございます。
>手順が端折りすぎていて全く理解出来ないです
拙いブログ記事で申し訳ないです。
ブログ全体としてブラッシュアップしていく所存です。
Very nice approach & documentation. Thanks bro.