new: extract WebView feature module

This commit is contained in:
2026-07-08 14:20:32 +02:00
parent f6a12ad84a
commit 0151d92b6a
23 changed files with 116 additions and 32 deletions
@@ -1,119 +0,0 @@
package bzh.ajaury.chombev.web.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.interop.UIKitView
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCSignatureOverride
import platform.CoreGraphics.CGRectZero
import platform.Foundation.NSError
import platform.Foundation.NSURL
import platform.Foundation.NSURLRequest
import platform.UIKit.UIUserInterfaceStyle
import platform.UIKit.UIUserInterfaceStyle.UIUserInterfaceStyleDark
import platform.UIKit.UIUserInterfaceStyle.UIUserInterfaceStyleLight
import platform.WebKit.WKNavigation
import platform.WebKit.WKNavigationDelegateProtocol
import platform.WebKit.WKWebView
import platform.WebKit.WKWebViewConfiguration
import platform.darwin.NSObject
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun PlatformWebView(
url: String,
offlineHtml: String?,
darkTheme: Boolean,
onLoadingStateChange: (Boolean) -> Unit,
modifier: Modifier,
) {
// WKWebView holds its navigationDelegate weakly, so keep a strong reference across recompositions.
val navigationDelegate = remember { WebViewNavigationDelegate() }
navigationDelegate.onLoadingStateChange = onLoadingStateChange
navigationDelegate.offlineHtml = offlineHtml
navigationDelegate.baseUrl = url
UIKitView(
modifier = modifier,
factory = {
WKWebView(
frame = CGRectZero.readValue(),
configuration = WKWebViewConfiguration(),
).apply {
navigationDelegate = navigationDelegate
setOverrideUserInterfaceStyle(darkTheme.toInterfaceStyle())
NSURL.URLWithString(url)?.let { nsUrl ->
loadRequest(NSURLRequest(uRL = nsUrl))
}
}
},
// Re-apply on recomposition so a light/dark switch takes effect without reloading the page.
update = { webView ->
webView.setOverrideUserInterfaceStyle(darkTheme.toInterfaceStyle())
},
)
}
/**
* Forwards WKWebView load lifecycle events to [onLoadingStateChange] so the UI can show a loader
* while a page is loading and hide it once loading finishes.
*
* When a navigation fails, it swaps in [offlineHtml] (loaded against [baseUrl] so links resolve) so
* the page still shows content offline. [showingOffline] guards against re-entering the fallback.
*/
private class WebViewNavigationDelegate :
NSObject(),
WKNavigationDelegateProtocol {
var onLoadingStateChange: (Boolean) -> Unit = {}
var offlineHtml: String? = null
var baseUrl: String = ""
private var showingOffline = false
override fun webView(
webView: WKWebView,
didStartProvisionalNavigation: WKNavigation?,
) {
onLoadingStateChange(true)
}
// Shares the (WKWebView, WKNavigation?) Kotlin signature with didStartProvisionalNavigation above.
@ObjCSignatureOverride
override fun webView(
webView: WKWebView,
didFinishNavigation: WKNavigation?,
) {
onLoadingStateChange(false)
}
override fun webView(
webView: WKWebView,
didFailNavigation: WKNavigation?,
withError: NSError,
) {
handleFailure(webView)
}
// Shares the (WKWebView, WKNavigation?, NSError) Kotlin signature with didFailNavigation above.
@ObjCSignatureOverride
override fun webView(
webView: WKWebView,
didFailProvisionalNavigation: WKNavigation?,
withError: NSError,
) {
handleFailure(webView)
}
private fun handleFailure(webView: WKWebView) {
val html = offlineHtml
if (html != null && !showingOffline) {
showingOffline = true
webView.loadHTMLString(string = html, baseURL = NSURL.URLWithString(baseUrl))
} else {
onLoadingStateChange(false)
}
}
}
private fun Boolean.toInterfaceStyle(): UIUserInterfaceStyle = if (this) UIUserInterfaceStyleDark else UIUserInterfaceStyleLight