You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by dp...@apache.org on 2026/06/11 18:09:15 UTC

(cordova-ios) branch master updated: fix(wkwebview): fix ViewController being the ScriptMessageHandler (#1664)

This is an automated email from the ASF dual-hosted git repository.

dpogue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-ios.git


The following commit(s) were added to refs/heads/master by this push:
     new 5669c2c9 fix(wkwebview): fix ViewController being the ScriptMessageHandler (#1664)
5669c2c9 is described below

commit 5669c2c9cb209f7ba6ecf70e1e2ea86b369fd459
Author: Darryl Pogue <da...@dpogue.ca>
AuthorDate: Thu Jun 11 11:09:09 2026 -0700

    fix(wkwebview): fix ViewController being the ScriptMessageHandler (#1664)
    
    We've had a mechanism for a while where if the ViewController is a
    WKScriptMessageHandler then it will be registered as the handler for
    Cordova actions rather than the default plugin implementation.
    
    Except there was a bug where it would try to register both of them with
    the same name and fail.
    
    This checks if the ViewController is a candidate first, and only
    registers the plugin as the script handler if it is not. Also add a unit
    test case to ensure that a ViewController that implements the
    WKScriptMessageHandler protocol correctly receives the script messages.
---
 .../Plugins/CDVWebViewEngine/CDVWebViewEngine.m    | 92 ++++++++++++++--------
 tests/CordovaLibTests/CDVWebViewEngineTests.swift  | 55 +++++++++++++
 .../CordovaTests.xcodeproj/project.pbxproj         |  4 +
 3 files changed, 119 insertions(+), 32 deletions(-)

diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m
index 36cda781..250f2a9b 100644
--- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m
+++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m
@@ -40,9 +40,6 @@
 @interface CDVWebViewEngine ()
 
 @property (nonatomic, strong, readwrite) UIView* engineWebView;
-@property (nonatomic, strong, readwrite) id <WKUIDelegate> uiDelegate;
-@property (nonatomic, weak) id <WKScriptMessageHandler> weakScriptMessageHandler;
-@property (nonatomic, strong) CDVURLSchemeHandler * schemeHandler;
 @property (nonatomic, readwrite) NSString *CDV_ASSETS_URL;
 @property (nonatomic, readwrite) Boolean cdvIsFileScheme;
 @property (nullable, nonatomic, strong, readwrite) WKWebViewConfiguration *configuration;
@@ -191,18 +188,37 @@
         self.CDV_ASSETS_URL = [NSString stringWithFormat:@"%@://%@", scheme, hostname];
     }
 
-    CDVWebViewUIDelegate* uiDelegate = [[CDVWebViewUIDelegate alloc] initWithViewController:self.viewController];
-    uiDelegate.title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
-    uiDelegate.mediaPermissionGrantType = [self parsePermissionGrantType:[settings cordovaSettingForKey:@"MediaPermissionGrantType"]];
-    uiDelegate.allowNewWindows = [settings cordovaBoolSettingForKey:@"AllowNewWindows" defaultValue:NO];
-    self.uiDelegate = uiDelegate;
 
-    CDVWebViewWeakScriptMessageHandler *weakScriptMessageHandler = [[CDVWebViewWeakScriptMessageHandler alloc] initWithScriptMessageHandler:self];
-
-    WKUserContentController* userContentController = [[WKUserContentController alloc] init];
+    WKUserContentController *userContentController = [[WKUserContentController alloc] init];
+
+    // This is where the Cordova magic happens...
+    //
+    // We register a script message handler that is exposed to the webview as
+    // `window.webkit.messageHandlers.cordova` and allows `postMessage()` to be
+    // called, which is how the `cordova.exec` bridge works for WKWebView to
+    // pass commands to Cordova plugins.
+    //
+    // If the view controller implements the WKScriptMessageHandler protocol,
+    // we allow that to be registered as the handler for Cordova messages,
+    // otherwise we register ourselves as the default implementation. This
+    // gives library consumers a way to override the default bridge behaviour
+    // if they need to.
+    //
+    // There's some indirection here with CDVWebViewWeakScriptMessageHandler to
+    // avoid retain cycles where the web view holds on to the view controller
+    // as a script handler while the view controller holds on to the web view
+    // as a child view.
+    CDVWebViewWeakScriptMessageHandler *weakScriptMessageHandler;
+    if ([self.viewController conformsToProtocol:@protocol(WKScriptMessageHandler)]) {
+        weakScriptMessageHandler = [[CDVWebViewWeakScriptMessageHandler alloc] initWithScriptMessageHandler:(id<WKScriptMessageHandler>)self.viewController];
+    } else {
+        weakScriptMessageHandler = [[CDVWebViewWeakScriptMessageHandler alloc] initWithScriptMessageHandler:self];
+    }
     [userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_BRIDGE_NAME];
 
-    if(self.CDV_ASSETS_URL) {
+    // If we have an asset URL, inject some JavaScript into the webview to
+    // make it available on the window object as a global variable
+    if (self.CDV_ASSETS_URL) {
         NSString *scriptCode = [NSString stringWithFormat:@"window.CDV_ASSETS_URL = '%@';", self.CDV_ASSETS_URL];
         WKUserScript *wkScript = [[WKUserScript alloc] initWithSource:scriptCode injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
 
@@ -211,18 +227,17 @@
         }
     }
 
-    WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings];
+    WKWebViewConfiguration *configuration = [self createConfigurationFromSettings:settings];
     configuration.userContentController = userContentController;
 
-    // Do not configure the scheme handler if the scheme is default (file)
-    if(!self.cdvIsFileScheme) {
-        self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithViewController:self.viewController];
-        [configuration setURLSchemeHandler:self.schemeHandler forURLScheme:scheme];
+    // Set up the scheme handler for the custom scheme (if we are not serving from a file:/// URL)
+    if (!self.cdvIsFileScheme) {
+        CDVURLSchemeHandler *schemeHandler = [[CDVURLSchemeHandler alloc] initWithViewController:self.viewController];
+        [configuration setURLSchemeHandler:schemeHandler forURLScheme:scheme];
     }
 
     // re-create WKWebView, since we need to update configuration
-    WKWebView* wkWebView = [[WKWebView alloc] initWithFrame:self.engineWebView.frame configuration:configuration];
-    wkWebView.UIDelegate = self.uiDelegate;
+    WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:self.engineWebView.frame configuration:configuration];
 
 #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160400
     // With the introduction of iOS 16.4 the webview is no longer inspectable by default.
@@ -238,31 +253,44 @@
     }
 #endif
 
-    /*
-     * This is where the "OverrideUserAgent" is handled. This will replace the entire UserAgent
-     * with the user defined custom UserAgent.
-     */
+    // This is where the "OverrideUserAgent" is handled. This will replace the entire UserAgent
+    // with the user defined custom UserAgent.
     if ([settings cordovaSettingForKey:@"OverrideUserAgent"] != nil) {
         wkWebView.customUserAgent = [settings cordovaSettingForKey:@"OverrideUserAgent"];
     }
 
-    [wkWebView addObserver:self forKeyPath:@"themeColor" options:NSKeyValueObservingOptionInitial context:nil];
+    // Hook up the web view's UI delegate
+    //
+    // The UI delegate is responsible for determining how the app implements
+    // things like web view alerts, prompts, permission requests, and new
+    // windows.
+    //
+    // If the view controller implements the WKUIDelegate protocol, we allow
+    // that to be registered as the UI delegate, otherwise we create a
+    // CDVWebViewUIDelegate instance. This gives library consumers a way to
+    // override the UI delegate and customize behaviour if needed.
+    if ([self.viewController conformsToProtocol:@protocol(WKUIDelegate)]) {
+        wkWebView.UIDelegate = (id<WKUIDelegate>)self.viewController;
+    } else {
+        CDVWebViewUIDelegate *uiDelegate = [[CDVWebViewUIDelegate alloc] initWithViewController:self.viewController];
 
-    self.engineWebView = wkWebView;
+        uiDelegate.title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
+        uiDelegate.mediaPermissionGrantType = [self parsePermissionGrantType:[settings cordovaSettingForKey:@"MediaPermissionGrantType"]];
+        uiDelegate.allowNewWindows = [settings cordovaBoolSettingForKey:@"AllowNewWindows" defaultValue:NO];
 
-    if ([self.viewController conformsToProtocol:@protocol(WKUIDelegate)]) {
-        wkWebView.UIDelegate = (id <WKUIDelegate>)self.viewController;
+        wkWebView.UIDelegate = uiDelegate;
     }
 
     if ([self.viewController conformsToProtocol:@protocol(WKNavigationDelegate)]) {
-        wkWebView.navigationDelegate = (id <WKNavigationDelegate>)self.viewController;
+        wkWebView.navigationDelegate = (id<WKNavigationDelegate>)self.viewController;
     } else {
-        wkWebView.navigationDelegate = (id <WKNavigationDelegate>)self;
+        wkWebView.navigationDelegate = self;
     }
 
-    if ([self.viewController conformsToProtocol:@protocol(WKScriptMessageHandler)]) {
-        [wkWebView.configuration.userContentController addScriptMessageHandler:(id < WKScriptMessageHandler >)self.viewController name:CDV_BRIDGE_NAME];
-    }
+    // Add an observer to react to themeColor changes for status bar colouring
+    [wkWebView addObserver:self forKeyPath:@"themeColor" options:NSKeyValueObservingOptionInitial context:nil];
+
+    self.engineWebView = wkWebView;
 
     [self updateSettings:settings];
 
diff --git a/tests/CordovaLibTests/CDVWebViewEngineTests.swift b/tests/CordovaLibTests/CDVWebViewEngineTests.swift
new file mode 100644
index 00000000..929acdaf
--- /dev/null
+++ b/tests/CordovaLibTests/CDVWebViewEngineTests.swift
@@ -0,0 +1,55 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+import XCTest
+import WebKit
+import Cordova
+@testable import CordovaApp
+
+class ScriptHandlingViewController: ViewController, WKScriptMessageHandler {
+    var expectation: XCTestExpectation? = nil
+
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if let expectation = self.expectation {
+            expectation.fulfill()
+        }
+
+        if let webView = self.webViewEngine.engineWebView as? WKWebView,
+           let scriptMessageHandler = self.webViewEngine as? WKScriptMessageHandler {
+            scriptMessageHandler.userContentController(webView.configuration.userContentController, didReceive:message)
+        }
+    }
+}
+
+class CDVWebViewEngineTests: XCTestCase {
+    @MainActor func testViewControllerScriptMessageHandler() {
+        let vc = ScriptHandlingViewController()
+        vc.webContentFolderName = "www"
+        vc.startPage = "index.html"
+        vc.expectation = expectation(description: "Script Handler expectation")
+
+        let appDelegate = UIApplication.shared.delegate as! AppDelegate
+        appDelegate.testWindow?.rootViewController = vc
+        appDelegate.testWindow?.makeKeyAndVisible()
+
+        waitForExpectations(timeout: 10.0, handler: nil)
+
+        appDelegate.testWindow?.rootViewController = UIViewController()
+    }
+}
diff --git a/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj b/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj
index 04755659..6a096353 100644
--- a/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj
+++ b/tests/CordovaLibTests/CordovaTests.xcodeproj/project.pbxproj
@@ -28,6 +28,7 @@
 		900A9F542CF0677F00FECE7A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900A9F532CF0677F00FECE7A /* ViewController.swift */; };
 		900A9F5A2CF0696300FECE7A /* SwiftInitPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900A9F582CF0695D00FECE7A /* SwiftInitPlugin.swift */; };
 		902530FC29A6DC53004AF1CF /* CDVPluginInitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 902530FA29A6DC53004AF1CF /* CDVPluginInitTests.m */; };
+		9021AA402FD8E41C00A950DD /* CDVWebViewEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9021AA3F2FD8E41500A950DD /* CDVWebViewEngineTests.swift */; };
 		905C8BA82CF0756E007EECEF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905C8BA72CF0756E007EECEF /* AppDelegate.swift */; };
 		9085EE302CF0617800603B73 /* Cordova.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0FA7CAC1E4BB6B30077B045 /* Cordova.framework */; };
 		90A775DD2C6F257500FC5BB0 /* CDVSettingsDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 90A775DC2C6F256D00FC5BB0 /* CDVSettingsDictionaryTests.m */; };
@@ -90,6 +91,7 @@
 		900A9F532CF0677F00FECE7A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
 		900A9F582CF0695D00FECE7A /* SwiftInitPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftInitPlugin.swift; sourceTree = "<group>"; };
 		902530FA29A6DC53004AF1CF /* CDVPluginInitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDVPluginInitTests.m; sourceTree = "<group>"; };
+		9021AA3F2FD8E41500A950DD /* CDVWebViewEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CDVWebViewEngineTests.swift; sourceTree = "<group>"; };
 		905C8BA72CF0756E007EECEF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		9085EE2F2CF05B5900603B73 /* CordovaTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CordovaTests.xctestplan; sourceTree = "<group>"; };
 		90A775DC2C6F256D00FC5BB0 /* CDVSettingsDictionaryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CDVSettingsDictionaryTests.m; sourceTree = "<group>"; };
@@ -190,6 +192,7 @@
 			isa = PBXGroup;
 			children = (
 				90D08FEC2F4E690700A9838F /* CDVSettingsDictionarySwiftTests.swift */,
+				9021AA3F2FD8E41500A950DD /* CDVWebViewEngineTests.swift */,
 				90D08FEA2F4E63CA00A9838F /* CDVURLSchemeHandlerTest.m */,
 				90A775DC2C6F256D00FC5BB0 /* CDVSettingsDictionaryTests.m */,
 				902530FA29A6DC53004AF1CF /* CDVPluginInitTests.m */,
@@ -353,6 +356,7 @@
 				C0FA7CBB1E4BBBBE0077B045 /* CDVViewControllerTest.m in Sources */,
 				90D08FEB2F4E63CA00A9838F /* CDVURLSchemeHandlerTest.m in Sources */,
 				C0FA7CBC1E4BBBBE0077B045 /* CDVInvokedUrlCommandTests.m in Sources */,
+				9021AA402FD8E41C00A950DD /* CDVWebViewEngineTests.swift in Sources */,
 				C0FA7CBF1E4BBBBE0077B045 /* CDVCommandDelegateTests.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org