2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【SwiftUI/JetpackCompose】Webビューをつくる ~比較で覚える宣言的モバイルUI~

Last updated at Posted at 2022-10-02

SwiftUIとJetpackComposeで、同セグメントの機能を両フレームワークでつくろうとした時に

  • きれいに対応関係がまとまっている
  • コピペで動く
  • 最もシンプルな実装

そんな記事があったらいいなと思い、備忘録も兼ねて自分が実装してみた範囲でまとめていきたいと思います。初心者ですので、より良い実装をご存知の方がいらっしゃいましたら、ご教示ください。

今回はWebビュー編です。

なお、見た目をSwiftUI寄りにしがちです(そっちしか知らないだけ...)

対応関係

【SwiftUI】WKWebViewをラップして使う
【JetpackCompose】AndroidViewのWebViewを使う

SwiftUI

SwiftUIでは、Web用のビューコンポーネントが用意されていないため、WebKitのWKWebViewをラップして使うというのが、現時点(20221002)での定番であるようでした。

Webの表示

WKWebViewをUIViewRepresentableでラップしたビューを作ります。

最小限のコードはこちら
WebView.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let urlString: String
    
    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        let url = URL(string: urlString)!
        let request = URLRequest(url: url)
        uiView.load(request)
    }
}

単に表示するだけでなく、スワイプでの戻る動作やURLごとに処理を分けるといったことをさせるには、Coordinatorを使ってUIViewからの情報を渡す必要があります。それを追加したものが以下になります。

応用的なコードはこちら
WebView.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let urlString: String
    
    class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate {
        var parent: WebView
        
        init(_ parent: WebView) {
            self.parent = parent
        }
        
        // "target="_blank""が設定されたリンクも開けるようにする(任意)
        func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
            if navigationAction.targetFrame == nil {
                webView.load(navigationAction.request)
            }
            return nil
        }
        
        // URLごとに処理を制御する(任意)
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
            if let url = navigationAction.request.url?.absoluteString {
                if (url.hasPrefix("https://apps.apple.com/")) {
                    guard let appStoreLink = URL(string: url) else {
                        return
                    }
                    UIApplication.shared.open(appStoreLink, options: [:], completionHandler: { (succes) in
                    })
                    decisionHandler(WKNavigationActionPolicy.cancel)
                } else if (url.hasPrefix("http")) {
                    decisionHandler(WKNavigationActionPolicy.allow)
                } else {
                    decisionHandler(WKNavigationActionPolicy.cancel)
                }
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.uiDelegate = context.coordinator
        uiView.navigationDelegate = context.coordinator
        
        // スワイプでの画面遷移
        uiView.allowsBackForwardNavigationGestures = true
        
        let url = URL(string: urlString)!
        let request = URLRequest(url: url)
        uiView.load(request)
    }
}

具体例

URLの文字列を引数にとって、以下のように使用します。

WebViewExample.swift
import SwiftUI

struct WebViewExample: View {
    var body: some View {
        WebView(urlString: "https://abc.xyz/")
    }
}

実行結果

無事にWebページが表示されました。
swiftui_webview

Jetpack Compose

AndroidViewという、従来型のView要素をComposeで扱えるようにするコンポーザブルがあり、それでWebViewを表示するということでした。

Webの表示

AndroidViewのWebViewを使って、汎用的なコンポーザブルを作ります。

最小限のコードはこちら
ComposeWebView.kt
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView

@Composable
fun ComposeWebView(
    modifier: Modifier = Modifier,
    url: String
) {
    AndroidView(
        modifier = modifier,
        factory = {
            WebView(it)
        },
        update = { webView ->
            webView.webViewClient = WebViewClient()
            webView.loadUrl(url)
        }
    )
}

戻るボタンの操作や、進行バーの表示、JavaScriptへの対応なども追加したい場合は、いろいろ追記する必要があります。以下がそれを追加した実装です。

応用的なコードはこちら
ComposeWebView.kt
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.launch

@Composable
fun ComposeWebView(
    modifier: Modifier = Modifier,
    url:String,
    onBack: (webView:WebView?) -> Unit = {},
    initSettings: (webSettings: WebSettings?) -> Unit = {},
    onProgressChange: (progress:Int)->Unit = {},
    onReceivedError: (error: WebResourceError?) -> Unit = {},
){
    // プログレスバーを表示したいときに使う
    val webViewChromeClient = object:WebChromeClient(){
        override fun onProgressChanged(view: WebView?, newProgress: Int) {
            onProgressChange(newProgress)
            super.onProgressChanged(view, newProgress)
        }
    }
    // 画面表示や通信に使う
    val webViewClient = object: WebViewClient(){
        override fun onPageStarted(view: WebView?, url: String?,
                                   favicon: Bitmap?) {
            super.onPageStarted(view, url, favicon)
            onProgressChange(-1)
        }
        override fun onPageFinished(view: WebView?, url: String?) {
            super.onPageFinished(view, url)
            onProgressChange(100)
        }
        override fun shouldOverrideUrlLoading(
            view: WebView?,
            request: WebResourceRequest?
        ): Boolean {
            if(null == request?.url) return false
            val showOverrideUrl = request.url.toString()
            try {
                if (!showOverrideUrl.startsWith("http://")
                    && !showOverrideUrl.startsWith("https://")) {
                    Intent(Intent.ACTION_VIEW, Uri.parse(showOverrideUrl)).apply {
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        view?.context?.applicationContext?.startActivity(this)
                    }
                    return true
                }
            } catch (e:Exception){
                return true
            }
            return super.shouldOverrideUrlLoading(view, request)
        }

        override fun onReceivedError(
            view: WebView?,
            request: WebResourceRequest?,
            error: WebResourceError?
        ) {
            super.onReceivedError(view, request, error)
            onReceivedError(error)
        }
    }

    var webView:WebView? = null
    val coroutineScope = rememberCoroutineScope()

    AndroidView(modifier = modifier,factory = { ctx ->
        WebView(ctx).apply {
            this.webViewClient = webViewClient
            this.webChromeClient = webViewChromeClient
            initSettings(this.settings)
            // JavaScriptの使用の許可
            this.settings.javaScriptEnabled = true
            webView = this
            loadUrl(url)
        }
    })

    BackHandler {
        coroutineScope.launch {
            onBack(webView)
        }
    }
}

なお、PreviewだとWebView.settingsにアクセスできないので、応用的なコードの場合にPreviewではエラーが出ますが、ビルドには問題ありません。

具体例

Webページを表示するには、まずAndroidManifest.xmlにインターネットの使用の許可を追記します。applicationタグの直前に書きました。
参考:パーミッション項目一覧

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

続いて、Webビューを呼び出すコンポーザブルをつくります。

WebViewExample.kt
@Composable
fun WebViewExample(modifier: Modifier = Modifier) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center,
    ) {
        ComposeWebView(url = "https://abc.xyz/")
    }
}

実行結果

こちらでも、無事にWebページが表示されました。
jetpack_compose_webview

まとめ

Webビュー
対応関係を再掲します。
【SwiftUI】WKWebViewをラップして使う
【JetpackCompose】AndroidViewのWebViewを使う

どちらの場合でも、従来型のWebビューをラップして使用する方法が定番のようでした。ゆくゆくはSwiftUIネイティブ、JetpackComposeネイティブなWebビューが登場するかもしれません。そうしたらもっと楽に実装できるので楽しみです。

参考

【SwiftUI】
wiii_naさんの記事
sh0さんの記事
yururiworkさんの記事
hfoasi8fje3さんの記事
How do I implement a WebView (WebKit) in SwiftUI? (Apple Developer Forum)

【Jetpack Compose】
katzさんの記事
TheMelodyさんのGist
Permissions webview android (StackOverflow)
WebViewClient・WebChromeClientの違いついて(TechForContributionさん)
How to implement javascript interface with Webview, when using Jetpack Compose? (StackOverflow)

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?