SwiftUI で WebView を表示する

現時点(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 標準で搭載されるといいですね。

3件のコメント

手順が端折りすぎていて全く理解出来ないです、、、特にWebViewContainer

ピエールさん

コメントありがとうございます。

>手順が端折りすぎていて全く理解出来ないです
拙いブログ記事で申し訳ないです。
ブログ全体としてブラッシュアップしていく所存です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です