diff --git a/gradle.properties b/gradle.properties index 0a1211bf..e262e319 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ kotlin.mpp.androidSourceSetLayoutVersion=2 android.useAndroidX=true android.compileSdk=36 android.targetSdk=36 -android.minSdk=21 +android.minSdk=24 #Versions GROUP=io.github.kevinnzou POM_ARTIFACT_ID=compose-webview-multiplatform diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt index e3a31a96..ba0d3a41 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt @@ -151,7 +151,7 @@ expect class WebViewFactoryParam /** * Platform specific parameters given to the WebView composable function: * - On Android, this contains an optional `AccompanistWebViewClient` and `AccompanistWebChromeClient` - * - On iOS, this is currently unused + * - On iOS, this contains an optional `WKNavigationDelegateProtocol` * - On Desktop, this is currently unused */ expect class PlatformWebViewParams diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt index bafa9da5..b1b53a49 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt @@ -14,7 +14,7 @@ import platform.darwin.NSObject /** * A JS message handler for WKWebView. */ -class WKJsMessageHandler( +final class WKJsMessageHandler( private val webViewJsBridge: WebViewJsBridge, ) : NSObject(), WKScriptMessageHandlerProtocol { diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt index 78b2e6bb..9cef9968 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt @@ -10,6 +10,7 @@ import kotlinx.cinterop.ObjCSignatureOverride import platform.CoreGraphics.CGPointMake import platform.Foundation.HTTPMethod import platform.Foundation.NSError +import platform.Foundation.NSSelectorFromString import platform.Foundation.allHTTPHeaderFields import platform.WebKit.WKNavigation import platform.WebKit.WKNavigationAction @@ -26,9 +27,10 @@ import platform.darwin.NSObject * Navigation delegate for the WKWebView */ @Suppress("CONFLICTING_OVERLOADS") -class WKNavigationDelegate( +final class WKNavigationDelegate( private val state: WebViewState, private val navigator: WebViewNavigator, + private val params: PlatformWebViewParams?, ) : NSObject(), WKNavigationDelegateProtocol { private var isRedirect = false @@ -36,6 +38,7 @@ class WKNavigationDelegate( /** * Called when the web view begins to receive web content. */ + @OptIn(ExperimentalForeignApi::class) @ObjCSignatureOverride override fun webView( webView: WKWebView, @@ -47,11 +50,44 @@ class WKNavigationDelegate( KLogger.info { "didStartProvisionalNavigation" } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:didStartProvisionalNavigation:"), + ) + ) { + delegate.webView(webView, didStartProvisionalNavigation = didStartProvisionalNavigation) + } + } + + /** + * Called when the web view receives a server redirect. + */ + @OptIn(ExperimentalForeignApi::class) + @ObjCSignatureOverride + override fun webView( + webView: WKWebView, + didReceiveServerRedirectForProvisionalNavigation: WKNavigation?, + ) { + isRedirect = true + KLogger.info { "didReceiveServerRedirectForProvisionalNavigation" } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:didReceiveServerRedirectForProvisionalNavigation:"), + ) + ) { + delegate.webView( + webView, + didReceiveServerRedirectForProvisionalNavigation = didReceiveServerRedirectForProvisionalNavigation, + ) + } } /** * Called when the web view receives a server redirect. */ + @OptIn(ExperimentalForeignApi::class) @ObjCSignatureOverride override fun webView( webView: WKWebView, @@ -64,6 +100,14 @@ class WKNavigationDelegate( "var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=${state.webSettings.zoomLevel}, maximum-scale=10.0, minimum-scale=0.1,user-scalable=$supportZoom');document.getElementsByTagName('head')[0].appendChild(meta);" webView.evaluateJavaScript(script) { _, _ -> } KLogger.info { "didCommitNavigation" } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:didCommitNavigation:"), + ) + ) { + delegate.webView(webView, didCommitNavigation = didCommitNavigation) + } } /** @@ -93,15 +137,60 @@ class WKNavigationDelegate( } } KLogger.info { "didFinishNavigation ${state.lastLoadedUrl}" } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:didFinishNavigation:"), + ) + ) { + delegate.webView(webView, didFinishNavigation = didFinishNavigation) + } } /** * Called when the web view fails to load content. */ + @OptIn(ExperimentalForeignApi::class) + @ObjCSignatureOverride override fun webView( webView: WKWebView, didFailProvisionalNavigation: WKNavigation?, withError: NSError, + ) { + KLogger.e { + "WebView Loading Failed with error: ${withError.localizedDescription}" + } + state.errorsForCurrentRequest.add( + WebViewError( + code = withError.code.toInt(), + description = withError.localizedDescription, + // on iOS all errors are from the main frame + isFromMainFrame = true, + ), + ) + KLogger.e { + "didFailProvisionalNavigation" + } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:didFailProvisionalNavigation:withError:"), + ) + ) { + delegate.webView( + webView, + didFailProvisionalNavigation = didFailProvisionalNavigation, + withError = withError, + ) + } + } + + @OptIn(ExperimentalForeignApi::class) + @ObjCSignatureOverride + override fun webView( + webView: WKWebView, + didFailNavigation: WKNavigation?, + withError: NSError, ) { KLogger.e { "WebView Loading Failed with error: ${withError.localizedDescription}" @@ -117,8 +206,36 @@ class WKNavigationDelegate( KLogger.e { "didFailNavigation" } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:didFailNavigation:withError:"), + ) + ) { + delegate.webView( + webView, + didFailNavigation = didFailNavigation, + withError = withError, + ) + } } + @OptIn(ExperimentalForeignApi::class) + override fun webViewWebContentProcessDidTerminate(webView: WKWebView) { + KLogger.e { + "webViewWebContentProcessDidTerminate" + } + val delegate = params?.delegate + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webViewWebContentProcessDidTerminate:"), + ) + ) { + delegate.webViewWebContentProcessDidTerminate(webView) + } + } + + @OptIn(ExperimentalForeignApi::class) override fun webView( webView: WKWebView, decidePolicyForNavigationAction: WKNavigationAction, @@ -128,6 +245,25 @@ class WKNavigationDelegate( KLogger.info { "Outer decidePolicyForNavigationAction: $url $isRedirect $decidePolicyForNavigationAction" } + + val delegate = params?.delegate + + fun callDelegateOrAllow() { + if (delegate != null && + (delegate as NSObject).respondsToSelector( + NSSelectorFromString("webView:decidePolicyForNavigationAction:decisionHandler:"), + ) + ) { + delegate.webView( + webView, + decidePolicyForNavigationAction = decidePolicyForNavigationAction, + decisionHandler = decisionHandler, + ) + } else { + decisionHandler(WKNavigationActionPolicy.WKNavigationActionPolicyAllow) + } + } + if ( url != null && !isRedirect && @@ -158,7 +294,7 @@ class WKNavigationDelegate( ) when (interceptResult) { is WebRequestInterceptResult.Allow -> { - decisionHandler(WKNavigationActionPolicy.WKNavigationActionPolicyAllow) + callDelegateOrAllow() } is WebRequestInterceptResult.Reject -> { @@ -177,7 +313,7 @@ class WKNavigationDelegate( } } else { isRedirect = false - decisionHandler(WKNavigationActionPolicy.WKNavigationActionPolicyAllow) + callDelegateOrAllow() } } } diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt index 32c63cdf..84928203 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt @@ -16,7 +16,7 @@ import platform.darwin.NSObject * Observer for the WKWebView's loading state */ @ExperimentalForeignApi -class WKWebViewObserver( +final class WKWebViewObserver( private val state: WebViewState, private val navigator: WebViewNavigator, ) : NSObject(), diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt index 6d9e0a7a..c1632479 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt @@ -20,6 +20,7 @@ import platform.Foundation.NSProcessInfo import platform.Foundation.setValue import platform.WebKit.WKAudiovisualMediaTypeAll import platform.WebKit.WKAudiovisualMediaTypeNone +import platform.WebKit.WKNavigationDelegateProtocol import platform.WebKit.WKWebView import platform.WebKit.WKWebViewConfiguration import platform.WebKit.javaScriptEnabled @@ -49,6 +50,7 @@ actual fun ActualWebView( onCreated = onCreated, onDispose = onDispose, factory = factory, + platformWebViewParams = platformWebViewParams, ) } @@ -57,7 +59,9 @@ actual data class WebViewFactoryParam( val config: WKWebViewConfiguration, ) -actual class PlatformWebViewParams +actual data class PlatformWebViewParams( + val delegate: WKNavigationDelegateProtocol? = null, +) /** Default WebView factory for iOS. */ @OptIn(ExperimentalForeignApi::class) @@ -81,6 +85,7 @@ fun IOSWebView( onCreated: (NativeWebView) -> Unit, onDispose: (NativeWebView) -> Unit, factory: (WebViewFactoryParam) -> NativeWebView, + platformWebViewParams: PlatformWebViewParams?, ) { val observer = remember { @@ -89,7 +94,7 @@ fun IOSWebView( navigator = navigator, ) } - val navigationDelegate = remember { WKNavigationDelegate(state, navigator) } + val navigationDelegate = remember { WKNavigationDelegate(state, navigator, platformWebViewParams) } val scope = rememberCoroutineScope() UIKitView(