From 436ef670549ee94a961d4e820d5d57bdcfb0651d Mon Sep 17 00:00:00 2001 From: "Tamburro, Michael (WM Technology)" Date: Fri, 11 Dec 2020 13:56:03 -0500 Subject: [PATCH 1/5] feat: add CDVWebViewEngineConfigurationDelegate to fully expose WKWebView configuration (#900) --- .../CDVWebViewEngine/CDVWebViewEngine.m | 18 +++++++++---- CordovaLib/Classes/Public/CDVViewController.h | 25 ++++++++++++++++++- CordovaLib/Classes/Public/CDVViewController.m | 7 +++--- .../Classes/Public/CDVWebViewEngineProtocol.h | 15 ++++++++--- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m index a07bf290a..029c2d9a8 100644 --- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m +++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m @@ -45,6 +45,7 @@ @interface CDVWebViewEngine () @property (nonatomic, strong) CDVURLSchemeHandler * schemeHandler; @property (nonatomic, readwrite) NSString *CDV_ASSETS_URL; @property (nonatomic, readwrite) Boolean cdvIsFileScheme; +@property (nullable, nonatomic, strong, readwrite) WKWebViewConfiguration *configuration; @end @@ -55,15 +56,16 @@ @implementation CDVWebViewEngine @synthesize engineWebView = _engineWebView; -- (instancetype)initWithFrame:(CGRect)frame +- (nullable instancetype)initWithFrame:(CGRect)frame configuration:(nullable WKWebViewConfiguration *)configuration { self = [super init]; if (self) { if (NSClassFromString(@"WKWebView") == nil) { return nil; } - - self.engineWebView = [[WKWebView alloc] initWithFrame:frame]; + + self.configuration = configuration; + self.engineWebView = configuration ? [[WKWebView alloc] initWithFrame:frame configuration:configuration] : [[WKWebView alloc] initWithFrame:frame]; } return self; @@ -71,8 +73,14 @@ - (instancetype)initWithFrame:(CGRect)frame - (WKWebViewConfiguration*) createConfigurationFromSettings:(NSDictionary*)settings { - WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; - configuration.processPool = [[CDVWebViewProcessPoolFactory sharedFactory] sharedProcessPool]; + WKWebViewConfiguration* configuration; + if (_configuration) { + configuration = _configuration; + } else { + configuration = [[WKWebViewConfiguration alloc] init]; + configuration.processPool = [[CDVWebViewProcessPoolFactory sharedFactory] sharedProcessPool]; + } + if (settings == nil) { return configuration; } diff --git a/CordovaLib/Classes/Public/CDVViewController.h b/CordovaLib/Classes/Public/CDVViewController.h index 4b5e423a4..cc92a829c 100644 --- a/CordovaLib/Classes/Public/CDVViewController.h +++ b/CordovaLib/Classes/Public/CDVViewController.h @@ -26,6 +26,25 @@ #import "CDVScreenOrientationDelegate.h" #import "CDVPlugin.h" #import "CDVWebViewEngineProtocol.h" +@import WebKit; + +@protocol CDVWebViewEngineConfigurationDelegate + +@optional +/// Provides a fully configured WKWebViewConfiguration which will be overriden with +/// any related settings you add to config.xml (e.g., `PreferredContentMode`). +/// Useful for more complex configuration, including websiteDataStore. +/// +/// Example usage: +/// +/// extension CDVViewController: CDVWebViewEngineConfigurationDelegate { +/// public func configuration() -> WKWebViewConfiguration { +/// // return your config here +/// } +/// } +- (nonnull WKWebViewConfiguration*)configuration; + +@end @interface CDVViewController : UIViewController { @protected @@ -36,9 +55,11 @@ CDVCommandQueue* _commandQueue; } +NS_ASSUME_NONNULL_BEGIN + @property (nonatomic, readonly, weak) IBOutlet UIView* webView; -@property (nonatomic, readonly, strong) NSMutableDictionary* pluginObjects; +@property (nullable, nonatomic, readonly, strong) NSMutableDictionary* pluginObjects; @property (nonatomic, readonly, strong) NSDictionary* pluginsMap; @property (nonatomic, readonly, strong) NSMutableDictionary* settings; @property (nonatomic, readonly, strong) NSXMLParser* configParser; @@ -74,4 +95,6 @@ - (void)showLaunchScreen:(BOOL)visible; +NS_ASSUME_NONNULL_END + @end diff --git a/CordovaLib/Classes/Public/CDVViewController.m b/CordovaLib/Classes/Public/CDVViewController.m index 9318d4651..415728348 100644 --- a/CordovaLib/Classes/Public/CDVViewController.m +++ b/CordovaLib/Classes/Public/CDVViewController.m @@ -27,7 +27,7 @@ Licensed to the Apache Software Foundation (ASF) under one #import "CDVCommandDelegateImpl.h" #import -@interface CDVViewController () { } +@interface CDVViewController () { } @property (nonatomic, readwrite, strong) NSXMLParser* configParser; @property (nonatomic, readwrite, strong) NSMutableDictionary* settings; @@ -505,9 +505,10 @@ - (UIView*)newCordovaViewWithFrame:(CGRect)bounds self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds]; } } else { - self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds]; + WKWebViewConfiguration *config = [self respondsToSelector:@selector(configuration)] ? [self configuration] : nil; + self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds configuration:config]; } - + if ([self.webViewEngine isKindOfClass:[CDVPlugin class]]) { [self registerPlugin:(CDVPlugin*)self.webViewEngine withClassName:webViewEngineClass]; } diff --git a/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h b/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h index e6126a4b4..b976fa707 100644 --- a/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h +++ b/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h @@ -18,6 +18,7 @@ */ #import +@import WebKit; #define kCDVWebViewEngineScriptMessageHandlers @"kCDVWebViewEngineScriptMessageHandlers" #define kCDVWebViewEngineWKNavigationDelegate @"kCDVWebViewEngineWKNavigationDelegate" @@ -26,16 +27,24 @@ @protocol CDVWebViewEngineProtocol +NS_ASSUME_NONNULL_BEGIN + @property (nonatomic, strong, readonly) UIView* engineWebView; - (id)loadRequest:(NSURLRequest*)request; -- (id)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL; -- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void (^)(id, NSError*))completionHandler; +- (id)loadHTMLString:(NSString*)string baseURL:(nullable NSURL*)baseURL; +- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void (^_Nullable)(id, NSError*))completionHandler; - (NSURL*)URL; - (BOOL)canLoadRequest:(NSURLRequest*)request; -- (instancetype)initWithFrame:(CGRect)frame; +/// Designated Initializer +/// @param frame The frame for the new web view. +/// @param configuration The configuration for the new web view. +- (nullable instancetype)initWithFrame:(CGRect)frame configuration:(nullable WKWebViewConfiguration *)configuration; + - (void)updateWithInfo:(NSDictionary*)info; +NS_ASSUME_NONNULL_END + @end From cecab7ce50b291b8238aaa155c5c44b7cd053183 Mon Sep 17 00:00:00 2001 From: "Tamburro, Michael (WM Technology)" Date: Tue, 5 Jan 2021 10:50:16 -0500 Subject: [PATCH 2/5] fix: update the existing tests of the default behavior --- tests/CordovaLibTests/CDVWebViewEngineTest.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CordovaLibTests/CDVWebViewEngineTest.m b/tests/CordovaLibTests/CDVWebViewEngineTest.m index 0facc70fa..1f00694bd 100644 --- a/tests/CordovaLibTests/CDVWebViewEngineTest.m +++ b/tests/CordovaLibTests/CDVWebViewEngineTest.m @@ -53,7 +53,7 @@ - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. // NOTE: no app settings are set, so it will rely on default WKWebViewConfiguration settings - self.plugin = [[CDVWebViewEngine alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + self.plugin = [[CDVWebViewEngine alloc] initWithFrame:CGRectMake(0, 0, 100, 100) configuration:nil]; XCTAssert([self.plugin conformsToProtocol:@protocol(CDVWebViewEngineProtocol)], @"Plugin does not conform to CDVWebViewEngineProtocol"); } @@ -126,7 +126,7 @@ - (void) testUpdateInfo { - (void) testConfigurationFromSettings { // we need to re-set the plugin from the "setup" to take in the app settings we need - self.plugin = [[CDVWebViewEngine alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + self.plugin = [[CDVWebViewEngine alloc] initWithFrame:CGRectMake(0, 0, 100, 100) configuration:nil]; self.viewController = [[CDVViewController alloc] init]; // generate the app settings From 3cd8bdac97169835e55eec023ceb2df812cad24c Mon Sep 17 00:00:00 2001 From: "Tamburro, Michael (WM Technology)" Date: Tue, 5 Jan 2021 12:31:48 -0500 Subject: [PATCH 3/5] chore: bring back initWithFrame, as existing CDVWebViewEngineProtocol conforming plugins use it --- .../Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m | 5 +++++ CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h | 3 ++- tests/CordovaLibTests/CDVWebViewEngineTest.m | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m index 029c2d9a8..5be8a41e7 100644 --- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m +++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m @@ -71,6 +71,11 @@ - (nullable instancetype)initWithFrame:(CGRect)frame configuration:(nullable WKW return self; } +- (nullable instancetype)initWithFrame:(CGRect)frame +{ + return [self initWithFrame:frame configuration:nil]; +} + - (WKWebViewConfiguration*) createConfigurationFromSettings:(NSDictionary*)settings { WKWebViewConfiguration* configuration; diff --git a/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h b/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h index b976fa707..8a562a704 100644 --- a/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h +++ b/CordovaLib/Classes/Public/CDVWebViewEngineProtocol.h @@ -37,8 +37,9 @@ NS_ASSUME_NONNULL_BEGIN - (NSURL*)URL; - (BOOL)canLoadRequest:(NSURLRequest*)request; +- (nullable instancetype)initWithFrame:(CGRect)frame; -/// Designated Initializer +/// Convenience Initializer /// @param frame The frame for the new web view. /// @param configuration The configuration for the new web view. - (nullable instancetype)initWithFrame:(CGRect)frame configuration:(nullable WKWebViewConfiguration *)configuration; diff --git a/tests/CordovaLibTests/CDVWebViewEngineTest.m b/tests/CordovaLibTests/CDVWebViewEngineTest.m index 1f00694bd..962d9434d 100644 --- a/tests/CordovaLibTests/CDVWebViewEngineTest.m +++ b/tests/CordovaLibTests/CDVWebViewEngineTest.m @@ -53,7 +53,7 @@ - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. // NOTE: no app settings are set, so it will rely on default WKWebViewConfiguration settings - self.plugin = [[CDVWebViewEngine alloc] initWithFrame:CGRectMake(0, 0, 100, 100) configuration:nil]; + self.plugin = [[CDVWebViewEngine alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; XCTAssert([self.plugin conformsToProtocol:@protocol(CDVWebViewEngineProtocol)], @"Plugin does not conform to CDVWebViewEngineProtocol"); } From d121e10cfd5236d376b69fc50a54e91513eee7fa Mon Sep 17 00:00:00 2001 From: "Tamburro, Michael (WM Technology)" Date: Wed, 6 Jan 2021 16:37:38 -0500 Subject: [PATCH 4/5] chore: clean up newCordovaViewWithFrame in order to extract initialization with configuration --- CordovaLib/Classes/Public/CDVViewController.m | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/CordovaLib/Classes/Public/CDVViewController.m b/CordovaLib/Classes/Public/CDVViewController.m index 415728348..8277b1627 100644 --- a/CordovaLib/Classes/Public/CDVViewController.m +++ b/CordovaLib/Classes/Public/CDVViewController.m @@ -485,37 +485,61 @@ - (BOOL)supportsOrientation:(UIInterfaceOrientation)orientation return [self.supportedOrientations containsObject:@(orientation)]; } -- (UIView*)newCordovaViewWithFrame:(CGRect)bounds +/// Retrieves the view from a newwly initialized webViewEngine +/// @param bounds The bounds with which the webViewEngine will be initialized +- (nonnull UIView*)newCordovaViewWithFrame:(CGRect)bounds { - NSString* defaultWebViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaDefaultWebViewEngine"]; - NSString* webViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaWebViewEngine"]; + NSString* defaultWebViewEngineClassName = [self.settings cordovaSettingForKey:@"CordovaDefaultWebViewEngine"]; + NSString* webViewEngineClassName = [self.settings cordovaSettingForKey:@"CordovaWebViewEngine"]; - if (!defaultWebViewEngineClass) { - defaultWebViewEngineClass = @"CDVWebViewEngine"; + if (!defaultWebViewEngineClassName) { + defaultWebViewEngineClassName = @"CDVWebViewEngine"; } - if (!webViewEngineClass) { - webViewEngineClass = defaultWebViewEngineClass; + if (!webViewEngineClassName) { + webViewEngineClassName = defaultWebViewEngineClassName; } - // Find webViewEngine - if (NSClassFromString(webViewEngineClass)) { - self.webViewEngine = [[NSClassFromString(webViewEngineClass) alloc] initWithFrame:bounds]; - // if a webView engine returns nil (not supported by the current iOS version) or doesn't conform to the protocol, or can't load the request, we use WKWebView - if (!self.webViewEngine || ![self.webViewEngine conformsToProtocol:@protocol(CDVWebViewEngineProtocol)] || ![self.webViewEngine canLoadRequest:[NSURLRequest requestWithURL:self.appUrl]]) { - self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds]; + // Determine if a provided custom web view engine is sufficient + id engine; + Class customWebViewEngineClass = NSClassFromString(webViewEngineClassName); + if (customWebViewEngineClass) { + id customWebViewEngine = [self initWebViewEngine:customWebViewEngineClass bounds:bounds]; + BOOL customConformsToProtocol = [customWebViewEngine conformsToProtocol:@protocol(CDVWebViewEngineProtocol)]; + BOOL customCanLoad = [customWebViewEngine canLoadRequest:[NSURLRequest requestWithURL:self.appUrl]]; + if (customConformsToProtocol && customCanLoad) { + engine = customWebViewEngine; } - } else { - WKWebViewConfiguration *config = [self respondsToSelector:@selector(configuration)] ? [self configuration] : nil; - self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds configuration:config]; } - if ([self.webViewEngine isKindOfClass:[CDVPlugin class]]) { - [self registerPlugin:(CDVPlugin*)self.webViewEngine withClassName:webViewEngineClass]; + // Otherwise use the default web view engine + if (!engine) { + Class defaultWebViewEngineClass = NSClassFromString(defaultWebViewEngineClassName); + id defaultWebViewEngine = [self initWebViewEngine:defaultWebViewEngineClass bounds:bounds]; + NSAssert([defaultWebViewEngine conformsToProtocol:@protocol(CDVWebViewEngineProtocol)], + @"we expected the default web view engine to conform to the CDVWebViewEngineProtocol"); + engine = defaultWebViewEngine; + } + + if ([engine isKindOfClass:[CDVPlugin class]]) { + [self registerPlugin:(CDVPlugin*)engine withClassName:webViewEngineClassName]; } + self.webViewEngine = engine; return self.webViewEngine.engineWebView; } +/// Initialiizes the webViewEngine, with config, if supported and provided +/// @param engineClass A class that must conform to the `CDVWebViewEngineProtocol` +/// @param bounds with which the webview will be initialized +- (id _Nullable) initWebViewEngine:(nonnull Class)engineClass bounds:(CGRect)bounds { + if ([engineClass respondsToSelector:@selector(initWithFrame:configuration:)]) { + WKWebViewConfiguration *config = [self respondsToSelector:@selector(configuration)] ? [self configuration] : nil; + return [[engineClass alloc] initWithFrame:bounds configuration:config]; + } else { + return [[engineClass alloc] initWithFrame:bounds]; + } +} + - (void)createLaunchView { CGRect webViewBounds = self.view.bounds; From 316411284e5af8d273a47e24445a2a64e076457a Mon Sep 17 00:00:00 2001 From: "Tamburro, Michael (WM Technology)" Date: Mon, 11 Jan 2021 13:20:27 -0500 Subject: [PATCH 5/5] chore: find existing methods that can return nil, and mark them as such for clarity --- CordovaLib/Classes/Public/CDVViewController.h | 10 +++++----- CordovaLib/Classes/Public/CDVViewController.m | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CordovaLib/Classes/Public/CDVViewController.h b/CordovaLib/Classes/Public/CDVViewController.h index cc92a829c..8074c8719 100644 --- a/CordovaLib/Classes/Public/CDVViewController.h +++ b/CordovaLib/Classes/Public/CDVViewController.h @@ -73,21 +73,21 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, strong) id commandDelegate; /** - Takes/Gives an array of UIInterfaceOrientation (int) objects - ex. UIInterfaceOrientationPortrait + Takes/Gives an array of UIInterfaceOrientation (int) objects + ex. UIInterfaceOrientationPortrait */ @property (nonatomic, readwrite, strong) NSArray* supportedOrientations; - (UIView*)newCordovaViewWithFrame:(CGRect)bounds; -- (NSString*)appURLScheme; -- (NSURL*)errorURL; +- (nullable NSString*)appURLScheme; +- (nullable NSURL*)errorURL; - (UIColor*)colorFromColorString:(NSString*)colorString CDV_DEPRECATED(7.0.0, "Use BackgroundColor in xcassets"); - (NSArray*)parseInterfaceOrientations:(NSArray*)orientations; - (BOOL)supportsOrientation:(UIInterfaceOrientation)orientation; -- (id)getCommandInstance:(NSString*)pluginName; +- (nullable id)getCommandInstance:(NSString*)pluginName; - (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className; - (void)registerPlugin:(CDVPlugin*)plugin withPluginName:(NSString*)pluginName; diff --git a/CordovaLib/Classes/Public/CDVViewController.m b/CordovaLib/Classes/Public/CDVViewController.m index 8277b1627..0781a3109 100644 --- a/CordovaLib/Classes/Public/CDVViewController.m +++ b/CordovaLib/Classes/Public/CDVViewController.m @@ -238,7 +238,7 @@ - (NSURL*)appUrl return appURL; } -- (NSURL*)errorURL +- (nullable NSURL*)errorURL { NSURL* errorUrl = nil; @@ -532,8 +532,8 @@ - (nonnull UIView*)newCordovaViewWithFrame:(CGRect)bounds /// @param engineClass A class that must conform to the `CDVWebViewEngineProtocol` /// @param bounds with which the webview will be initialized - (id _Nullable) initWebViewEngine:(nonnull Class)engineClass bounds:(CGRect)bounds { - if ([engineClass respondsToSelector:@selector(initWithFrame:configuration:)]) { - WKWebViewConfiguration *config = [self respondsToSelector:@selector(configuration)] ? [self configuration] : nil; + WKWebViewConfiguration *config = [self respondsToSelector:@selector(configuration)] ? [self configuration] : nil; + if (config && [engineClass respondsToSelector:@selector(initWithFrame:configuration:)]) { return [[engineClass alloc] initWithFrame:bounds configuration:config]; } else { return [[engineClass alloc] initWithFrame:bounds]; @@ -632,7 +632,7 @@ - (void)registerPlugin:(CDVPlugin*)plugin withPluginName:(NSString*)pluginName /** Returns an instance of a CordovaCommand object, based on its name. If one exists already, it is returned. */ -- (id)getCommandInstance:(NSString*)pluginName +- (nullable id)getCommandInstance:(NSString*)pluginName { // first, we try to find the pluginName in the pluginsMap // (acts as a whitelist as well) if it does not exist, we return nil @@ -666,7 +666,7 @@ - (id)getCommandInstance:(NSString*)pluginName #pragma mark - -- (NSString*)appURLScheme +- (nullable NSString*)appURLScheme { NSString* URLScheme = nil;