| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "FlutterWebView.h" |
| #import "FLTWKNavigationDelegate.h" |
| #import "FLTWKProgressionDelegate.h" |
| #import "FlutterWebView_Test.h" |
| #import "JavaScriptChannelHandler.h" |
| |
| @implementation FLTWebViewFactory { |
| NSObject<FlutterBinaryMessenger> *_messenger; |
| FLTCookieManager *_cookieManager; |
| } |
| |
| - (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger |
| cookieManager:(FLTCookieManager *)cookieManager { |
| self = [super init]; |
| if (self) { |
| _messenger = messenger; |
| _cookieManager = cookieManager; |
| } |
| return self; |
| } |
| |
| - (NSObject<FlutterMessageCodec> *)createArgsCodec { |
| return [FlutterStandardMessageCodec sharedInstance]; |
| } |
| |
| - (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame |
| viewIdentifier:(int64_t)viewId |
| arguments:(id _Nullable)args { |
| if (@available(iOS 11.0, *)) { |
| [_cookieManager setCookiesForData:args[@"cookies"]]; |
| } |
| |
| FLTWebViewController *webviewController = [[FLTWebViewController alloc] initWithFrame:frame |
| viewIdentifier:viewId |
| arguments:args |
| binaryMessenger:_messenger]; |
| return webviewController; |
| } |
| |
| @end |
| |
| @implementation FLTWKWebView |
| |
| - (void)setFrame:(CGRect)frame { |
| [super setFrame:frame]; |
| self.scrollView.contentInset = UIEdgeInsetsZero; |
| // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of |
| // webview's contentInsets. |
| // self.scrollView.contentInset = UIEdgeInsetsZero; |
| if (@available(iOS 11, *)) { |
| // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will |
| // always be 0. |
| if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.adjustedContentInset, UIEdgeInsetsZero)) { |
| return; |
| } |
| UIEdgeInsets insetToAdjust = self.scrollView.adjustedContentInset; |
| self.scrollView.contentInset = UIEdgeInsetsMake(-insetToAdjust.top, -insetToAdjust.left, |
| -insetToAdjust.bottom, -insetToAdjust.right); |
| } |
| } |
| |
| @end |
| |
| @implementation FLTWebViewController { |
| FLTWKWebView *_webView; |
| int64_t _viewId; |
| FlutterMethodChannel *_channel; |
| NSString *_currentUrl; |
| // The set of registered JavaScript channel names. |
| NSMutableSet *_javaScriptChannelNames; |
| FLTWKNavigationDelegate *_navigationDelegate; |
| FLTWKProgressionDelegate *_progressionDelegate; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame |
| viewIdentifier:(int64_t)viewId |
| arguments:(id _Nullable)args |
| binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger { |
| if (self = [super init]) { |
| _viewId = viewId; |
| |
| NSString *channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId]; |
| _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; |
| _javaScriptChannelNames = [[NSMutableSet alloc] init]; |
| |
| WKUserContentController *userContentController = [[WKUserContentController alloc] init]; |
| if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { |
| NSArray *javaScriptChannelNames = args[@"javascriptChannelNames"]; |
| [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; |
| [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; |
| } |
| |
| NSDictionary<NSString *, id> *settings = args[@"settings"]; |
| |
| WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; |
| [self applyConfigurationSettings:settings toConfiguration:configuration]; |
| configuration.userContentController = userContentController; |
| [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] |
| inConfiguration:configuration]; |
| |
| _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; |
| |
| // Background color |
| NSNumber *backgroundColorNSNumber = args[@"backgroundColor"]; |
| if ([backgroundColorNSNumber isKindOfClass:[NSNumber class]]) { |
| int backgroundColorInt = [backgroundColorNSNumber intValue]; |
| UIColor *backgroundColor = [UIColor colorWithRed:(backgroundColorInt >> 16 & 0xff) / 255.0 |
| green:(backgroundColorInt >> 8 & 0xff) / 255.0 |
| blue:(backgroundColorInt & 0xff) / 255.0 |
| alpha:(backgroundColorInt >> 24 & 0xff) / 255.0]; |
| _webView.opaque = NO; |
| _webView.backgroundColor = UIColor.clearColor; |
| _webView.scrollView.backgroundColor = backgroundColor; |
| } |
| |
| _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; |
| _webView.UIDelegate = self; |
| _webView.navigationDelegate = _navigationDelegate; |
| __weak __typeof__(self) weakSelf = self; |
| [_channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { |
| [weakSelf onMethodCall:call result:result]; |
| }]; |
| |
| if (@available(iOS 11.0, *)) { |
| _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; |
| if (@available(iOS 13.0, *)) { |
| _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; |
| } |
| } |
| |
| [self applySettings:settings]; |
| // TODO(amirh): return an error if apply settings failed once it's possible to do so. |
| // https://github.com/flutter/flutter/issues/36228 |
| |
| NSString *initialUrl = args[@"initialUrl"]; |
| if ([initialUrl isKindOfClass:[NSString class]]) { |
| NSURL *url = [NSURL URLWithString:initialUrl]; |
| if (url) { |
| NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; |
| [_webView loadRequest:request]; |
| } |
| } |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| if (_progressionDelegate != nil) { |
| [_progressionDelegate stopObservingProgress:_webView]; |
| } |
| } |
| |
| - (UIView *)view { |
| return _webView; |
| } |
| |
| - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { |
| if ([[call method] isEqualToString:@"updateSettings"]) { |
| [self onUpdateSettings:call result:result]; |
| } else if ([[call method] isEqualToString:@"loadFile"]) { |
| [self onLoadFile:call result:result]; |
| } else if ([[call method] isEqualToString:@"loadFlutterAsset"]) { |
| [self onLoadFlutterAsset:call result:result]; |
| } else if ([[call method] isEqualToString:@"loadHtmlString"]) { |
| [self onLoadHtmlString:call result:result]; |
| } else if ([[call method] isEqualToString:@"loadUrl"]) { |
| [self onLoadUrl:call result:result]; |
| } else if ([[call method] isEqualToString:@"loadRequest"]) { |
| [self onLoadRequest:call result:result]; |
| } else if ([[call method] isEqualToString:@"canGoBack"]) { |
| [self onCanGoBack:call result:result]; |
| } else if ([[call method] isEqualToString:@"canGoForward"]) { |
| [self onCanGoForward:call result:result]; |
| } else if ([[call method] isEqualToString:@"goBack"]) { |
| [self onGoBack:call result:result]; |
| } else if ([[call method] isEqualToString:@"goForward"]) { |
| [self onGoForward:call result:result]; |
| } else if ([[call method] isEqualToString:@"reload"]) { |
| [self onReload:call result:result]; |
| } else if ([[call method] isEqualToString:@"currentUrl"]) { |
| [self onCurrentUrl:call result:result]; |
| } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { |
| [self onEvaluateJavaScript:call result:result]; |
| } else if ([[call method] isEqualToString:@"runJavascript"]) { |
| [self onRunJavaScript:call result:result sendReturnValue:NO]; |
| } else if ([[call method] isEqualToString:@"runJavascriptReturningResult"]) { |
| [self onRunJavaScript:call result:result sendReturnValue:YES]; |
| } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { |
| [self onAddJavaScriptChannels:call result:result]; |
| } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { |
| [self onRemoveJavaScriptChannels:call result:result]; |
| } else if ([[call method] isEqualToString:@"clearCache"]) { |
| [self clearCache:result]; |
| } else if ([[call method] isEqualToString:@"getTitle"]) { |
| [self onGetTitle:result]; |
| } else if ([[call method] isEqualToString:@"scrollTo"]) { |
| [self onScrollTo:call result:result]; |
| } else if ([[call method] isEqualToString:@"scrollBy"]) { |
| [self onScrollBy:call result:result]; |
| } else if ([[call method] isEqualToString:@"getScrollX"]) { |
| [self getScrollX:call result:result]; |
| } else if ([[call method] isEqualToString:@"getScrollY"]) { |
| [self getScrollY:call result:result]; |
| } else { |
| result(FlutterMethodNotImplemented); |
| } |
| } |
| |
| - (void)onUpdateSettings:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSString *error = [self applySettings:[call arguments]]; |
| if (error == nil) { |
| result(nil); |
| return; |
| } |
| result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); |
| } |
| |
| - (void)onLoadFile:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSString *error = nil; |
| if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) { |
| result([FlutterError errorWithCode:@"loadFile_failed" |
| message:@"Failed parsing file path." |
| details:error]); |
| return; |
| } |
| |
| NSURL *url = [NSURL fileURLWithPath:[call arguments] isDirectory:NO]; |
| |
| if (!url) { |
| NSString *errorDetails = [NSString stringWithFormat:@"Initializing NSURL with the supplied " |
| @"'%@' path resulted in a nil value.", |
| [call arguments]]; |
| result([FlutterError errorWithCode:@"loadFile_failed" |
| message:@"Failed parsing file path." |
| details:errorDetails]); |
| return; |
| } |
| |
| NSURL *baseUrl = [url URLByDeletingLastPathComponent]; |
| |
| [_webView loadFileURL:url allowingReadAccessToURL:baseUrl]; |
| result(nil); |
| } |
| |
| - (void)onLoadFlutterAsset:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSString *error = nil; |
| if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) { |
| result([FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" |
| message:@"Supplied asset key is not valid." |
| details:error]); |
| return; |
| } |
| |
| NSString *assetKey = [call arguments]; |
| NSString *assetFilePath = [FlutterDartProject lookupKeyForAsset:assetKey]; |
| NSURL *url = [[NSBundle mainBundle] URLForResource:[assetFilePath stringByDeletingPathExtension] |
| withExtension:assetFilePath.pathExtension]; |
| |
| if (!url) { |
| result([FlutterError |
| errorWithCode:@"loadFlutterAsset_invalidKey" |
| message:@"Failed parsing file path for supplied key." |
| details:[NSString |
| stringWithFormat:@"Failed to convert path '%@' into NSURL for key '%@'.", |
| assetFilePath, assetKey]]); |
| return; |
| } |
| |
| [_webView loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]; |
| result(nil); |
| } |
| |
| - (void)onLoadHtmlString:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSDictionary *arguments = [call arguments]; |
| if (![arguments isKindOfClass:NSDictionary.class]) { |
| result([FlutterError |
| errorWithCode:@"loadHtmlString_failed" |
| message:@"Failed parsing arguments." |
| details:@"Arguments should be a dictionary containing at least a 'html' element and " |
| @"optionally a 'baseUrl' argument. For example: `@{ @\"html\": @\"some html " |
| @"code\", @\"baseUrl\": @\"https://flutter.dev\" }`"]); |
| return; |
| } |
| |
| NSString *htmlString = [call arguments][@"html"]; |
| NSString *baseUrl = |
| [call arguments][@"baseUrl"] == [NSNull null] ? nil : [call arguments][@"baseUrl"]; |
| NSString *error = nil; |
| if (![FLTWebViewController isValidStringArgument:htmlString withErrorMessage:&error]) { |
| result([FlutterError errorWithCode:@"loadHtmlString_failed" |
| message:@"Failed parsing HTML string argument." |
| details:error]); |
| return; |
| } |
| |
| [_webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:baseUrl]]; |
| result(nil); |
| } |
| |
| - (void)onLoadUrl:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSMutableDictionary *requestData = [[NSMutableDictionary alloc] init]; |
| if (call.arguments[@"url"]) { |
| requestData[@"uri"] = call.arguments[@"url"]; |
| } |
| if (call.arguments[@"headers"]) { |
| requestData[@"headers"] = call.arguments[@"headers"]; |
| } |
| NSURLRequest *request = [self buildNSURLRequest:@{@"request" : requestData}]; |
| if (!request) { |
| result([FlutterError |
| errorWithCode:@"loadUrl_failed" |
| message:@"Failed parsing the URL" |
| details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); |
| } else { |
| [_webView loadRequest:request]; |
| result(nil); |
| } |
| } |
| |
| - (void)onLoadRequest:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSURLRequest *request = [self buildNSURLRequest:[call arguments]]; |
| if (!request) { |
| result([FlutterError |
| errorWithCode:@"loadRequest_failed" |
| message:@"Failed parsing the URL" |
| details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); |
| } else { |
| [_webView loadRequest:request]; |
| result(nil); |
| } |
| } |
| |
| - (void)onCanGoBack:(FlutterMethodCall *)call result:(FlutterResult)result { |
| BOOL canGoBack = [_webView canGoBack]; |
| result(@(canGoBack)); |
| } |
| |
| - (void)onCanGoForward:(FlutterMethodCall *)call result:(FlutterResult)result { |
| BOOL canGoForward = [_webView canGoForward]; |
| result(@(canGoForward)); |
| } |
| |
| - (void)onGoBack:(FlutterMethodCall *)call result:(FlutterResult)result { |
| [_webView goBack]; |
| result(nil); |
| } |
| |
| - (void)onGoForward:(FlutterMethodCall *)call result:(FlutterResult)result { |
| [_webView goForward]; |
| result(nil); |
| } |
| |
| - (void)onReload:(FlutterMethodCall *)call result:(FlutterResult)result { |
| [_webView reload]; |
| result(nil); |
| } |
| |
| - (void)onCurrentUrl:(FlutterMethodCall *)call result:(FlutterResult)result { |
| _currentUrl = [[_webView URL] absoluteString]; |
| result(_currentUrl); |
| } |
| |
| - (void)onEvaluateJavaScript:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSString *jsString = [call arguments]; |
| if (!jsString) { |
| result([FlutterError errorWithCode:@"evaluateJavaScript_failed" |
| message:@"JavaScript String cannot be null" |
| details:nil]); |
| return; |
| } |
| [_webView evaluateJavaScript:jsString |
| completionHandler:^(_Nullable id evaluateResult, NSError *_Nullable error) { |
| if (error) { |
| result([FlutterError |
| errorWithCode:@"evaluateJavaScript_failed" |
| message:@"Failed evaluating JavaScript" |
| details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@", |
| jsString, error]]); |
| } else { |
| result([NSString stringWithFormat:@"%@", evaluateResult]); |
| } |
| }]; |
| } |
| |
| - (void)onRunJavaScript:(FlutterMethodCall *)call |
| result:(FlutterResult)result |
| sendReturnValue:(BOOL)sendReturnValue { |
| NSString *jsString = [call arguments]; |
| if (!jsString) { |
| result([FlutterError errorWithCode:@"runJavascript_failed" |
| message:@"JavaScript String cannot be null" |
| details:nil]); |
| return; |
| } |
| [_webView |
| evaluateJavaScript:jsString |
| completionHandler:^(_Nullable id evaluateResult, NSError *_Nullable error) { |
| if (error) { |
| // WebKit will throw an error (WKErrorJavaScriptResultTypeIsUnsupported) when the |
| // type of the evaluated value is unsupported. This also goes for |
| // `null` and `undefined` on iOS 14+, for example when running a void function. |
| // For ease of use this specific error is ignored when no return value is expected. |
| BOOL sendError = |
| sendReturnValue || error.code != WKErrorJavaScriptResultTypeIsUnsupported; |
| result(sendError |
| ? [FlutterError |
| errorWithCode:(sendReturnValue ? @"runJavascriptReturningResult_failed" |
| : @"runJavascript_failed") |
| message:@"Failed running JavaScript" |
| details:[NSString |
| stringWithFormat:@"JavaScript string was: '%@'\n%@", |
| jsString, error]] |
| : nil); |
| return; |
| } |
| result(sendReturnValue ? [NSString stringWithFormat:@"%@", evaluateResult] : nil); |
| }]; |
| } |
| |
| - (void)onAddJavaScriptChannels:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSArray *channelNames = [call arguments]; |
| NSSet *channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; |
| [_javaScriptChannelNames addObjectsFromArray:channelNames]; |
| [self registerJavaScriptChannels:channelNamesSet |
| controller:_webView.configuration.userContentController]; |
| result(nil); |
| } |
| |
| - (void)onRemoveJavaScriptChannels:(FlutterMethodCall *)call result:(FlutterResult)result { |
| // WkWebView does not support removing a single user script, so instead we remove all |
| // user scripts, all message handlers. And re-register channels that shouldn't be removed. |
| [_webView.configuration.userContentController removeAllUserScripts]; |
| for (NSString *channelName in _javaScriptChannelNames) { |
| [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName]; |
| } |
| |
| NSArray *channelNamesToRemove = [call arguments]; |
| for (NSString *channelName in channelNamesToRemove) { |
| [_javaScriptChannelNames removeObject:channelName]; |
| } |
| |
| [self registerJavaScriptChannels:_javaScriptChannelNames |
| controller:_webView.configuration.userContentController]; |
| result(nil); |
| } |
| |
| - (void)clearCache:(FlutterResult)result { |
| NSSet *cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; |
| WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; |
| NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; |
| [dataStore removeDataOfTypes:cacheDataTypes |
| modifiedSince:dateFrom |
| completionHandler:^{ |
| result(nil); |
| }]; |
| } |
| |
| - (void)onGetTitle:(FlutterResult)result { |
| NSString *title = _webView.title; |
| result(title); |
| } |
| |
| - (void)onScrollTo:(FlutterMethodCall *)call result:(FlutterResult)result { |
| NSDictionary *arguments = [call arguments]; |
| int x = [arguments[@"x"] intValue]; |
| int y = [arguments[@"y"] intValue]; |
| |
| _webView.scrollView.contentOffset = CGPointMake(x, y); |
| result(nil); |
| } |
| |
| - (void)onScrollBy:(FlutterMethodCall *)call result:(FlutterResult)result { |
| CGPoint contentOffset = _webView.scrollView.contentOffset; |
| |
| NSDictionary *arguments = [call arguments]; |
| int x = [arguments[@"x"] intValue] + contentOffset.x; |
| int y = [arguments[@"y"] intValue] + contentOffset.y; |
| |
| _webView.scrollView.contentOffset = CGPointMake(x, y); |
| result(nil); |
| } |
| |
| - (void)getScrollX:(FlutterMethodCall *)call result:(FlutterResult)result { |
| int offsetX = _webView.scrollView.contentOffset.x; |
| result(@(offsetX)); |
| } |
| |
| - (void)getScrollY:(FlutterMethodCall *)call result:(FlutterResult)result { |
| int offsetY = _webView.scrollView.contentOffset.y; |
| result(@(offsetY)); |
| } |
| |
| // Returns nil when successful, or an error message when one or more keys are unknown. |
| - (NSString *)applySettings:(NSDictionary<NSString *, id> *)settings { |
| NSMutableArray<NSString *> *unknownKeys = [[NSMutableArray alloc] init]; |
| for (NSString *key in settings) { |
| if ([key isEqualToString:@"jsMode"]) { |
| NSNumber *mode = settings[key]; |
| [self updateJsMode:mode]; |
| } else if ([key isEqualToString:@"hasNavigationDelegate"]) { |
| NSNumber *hasDartNavigationDelegate = settings[key]; |
| _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; |
| } else if ([key isEqualToString:@"hasProgressTracking"]) { |
| NSNumber *hasProgressTrackingValue = settings[key]; |
| bool hasProgressTracking = [hasProgressTrackingValue boolValue]; |
| if (hasProgressTracking) { |
| _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView |
| channel:_channel]; |
| } |
| } else if ([key isEqualToString:@"debuggingEnabled"]) { |
| // no-op debugging is always enabled on iOS. |
| } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { |
| NSNumber *allowsBackForwardNavigationGestures = settings[key]; |
| _webView.allowsBackForwardNavigationGestures = |
| [allowsBackForwardNavigationGestures boolValue]; |
| } else if ([key isEqualToString:@"userAgent"]) { |
| NSString *userAgent = settings[key]; |
| [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; |
| } else if ([key isEqualToString:@"zoomEnabled"]) { |
| NSNumber *zoomEnabled = settings[key]; |
| _navigationDelegate.shouldEnableZoom = [zoomEnabled boolValue]; |
| } else { |
| [unknownKeys addObject:key]; |
| } |
| } |
| if ([unknownKeys count] == 0) { |
| return nil; |
| } |
| return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", |
| [unknownKeys componentsJoinedByString:@", "]]; |
| } |
| |
| - (void)applyConfigurationSettings:(NSDictionary<NSString *, id> *)settings |
| toConfiguration:(WKWebViewConfiguration *)configuration { |
| NSAssert(configuration != _webView.configuration, |
| @"configuration needs to be updated before webView.configuration."); |
| for (NSString *key in settings) { |
| if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { |
| NSNumber *allowsInlineMediaPlayback = settings[key]; |
| configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; |
| } |
| } |
| } |
| |
| - (void)updateJsMode:(NSNumber *)mode { |
| WKPreferences *preferences = [[_webView configuration] preferences]; |
| switch ([mode integerValue]) { |
| case 0: // disabled |
| [preferences setJavaScriptEnabled:NO]; |
| break; |
| case 1: // unrestricted |
| [preferences setJavaScriptEnabled:YES]; |
| break; |
| default: |
| NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode); |
| } |
| } |
| |
| - (void)updateAutoMediaPlaybackPolicy:(NSNumber *)policy |
| inConfiguration:(WKWebViewConfiguration *)configuration { |
| switch ([policy integerValue]) { |
| case 0: // require_user_action_for_all_media_types |
| if (@available(iOS 10.0, *)) { |
| configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; |
| } else { |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| configuration.requiresUserActionForMediaPlayback = true; |
| #pragma clang diagnostic pop |
| } |
| break; |
| case 1: // always_allow |
| if (@available(iOS 10.0, *)) { |
| configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; |
| } else { |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| configuration.requiresUserActionForMediaPlayback = false; |
| #pragma clang diagnostic pop |
| } |
| break; |
| default: |
| NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); |
| } |
| } |
| |
| /** |
| * Parses the method call arguments and converts them to an NSURLRequest object. |
| * |
| * @param arguments the method call arguments. |
| * |
| * @return NSURLRequest object. |
| */ |
| - (NSURLRequest *)buildNSURLRequest:(NSDictionary<NSString *, id> *)arguments { |
| id requestParameters = arguments[@"request"]; |
| if (![requestParameters isKindOfClass:[NSDictionary class]]) { |
| return nil; |
| } |
| |
| NSString *urlString = requestParameters[@"uri"]; |
| if (!urlString) { |
| return nil; |
| } |
| |
| NSURL *url = [NSURL URLWithString:urlString]; |
| if (!url) { |
| return nil; |
| } |
| |
| NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; |
| |
| NSString *httpMethod = requestParameters[@"method"]; |
| if (httpMethod) { |
| [request setHTTPMethod:httpMethod]; |
| } |
| |
| id httpBody = requestParameters[@"body"]; |
| if ([httpBody isKindOfClass:[FlutterStandardTypedData class]]) { |
| [request setHTTPBody:[httpBody data]]; |
| } |
| |
| id headers = requestParameters[@"headers"]; |
| if ([headers isKindOfClass:[NSDictionary class]]) { |
| [request setAllHTTPHeaderFields:headers]; |
| } |
| |
| return request; |
| } |
| |
| - (void)registerJavaScriptChannels:(NSSet *)channelNames |
| controller:(WKUserContentController *)userContentController { |
| for (NSString *channelName in channelNames) { |
| FLTJavaScriptChannel *channel = |
| [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel |
| javaScriptChannelName:channelName]; |
| [userContentController addScriptMessageHandler:channel name:channelName]; |
| NSString *wrapperSource = [NSString |
| stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName]; |
| WKUserScript *wrapperScript = |
| [[WKUserScript alloc] initWithSource:wrapperSource |
| injectionTime:WKUserScriptInjectionTimeAtDocumentStart |
| forMainFrameOnly:NO]; |
| [userContentController addUserScript:wrapperScript]; |
| } |
| } |
| |
| - (void)updateUserAgent:(NSString *)userAgent { |
| [_webView setCustomUserAgent:userAgent]; |
| } |
| |
| /** |
| * Validates if the given `argument` is a non-null, non-empty string. |
| * |
| * @param argument The argument that should be validated. |
| * @param errorDetails An optional NSString variable which will contain a detailed error message in |
| * case the supplied argument is not valid. |
| * @return `YES` if the given `argument` is a valid non-null, non-empty string; otherwise `NO`. |
| */ |
| + (BOOL)isValidStringArgument:(id)argument withErrorMessage:(NSString **)errorDetails { |
| if (!argument) { |
| if (errorDetails) { |
| *errorDetails = @"Argument is nil."; |
| } |
| return NO; |
| } |
| if (![argument isKindOfClass:NSString.class]) { |
| if (errorDetails) { |
| *errorDetails = @"Argument is not of type NSString."; |
| } |
| return NO; |
| } |
| if (![argument length]) { |
| if (errorDetails) { |
| *errorDetails = @"Argument contains an empty string."; |
| } |
| return NO; |
| } |
| |
| return YES; |
| } |
| |
| #pragma mark WKUIDelegate |
| |
| - (WKWebView *)webView:(WKWebView *)webView |
| createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration |
| forNavigationAction:(WKNavigationAction *)navigationAction |
| windowFeatures:(WKWindowFeatures *)windowFeatures { |
| if (!navigationAction.targetFrame.isMainFrame) { |
| [webView loadRequest:navigationAction.request]; |
| } |
| |
| return nil; |
| } |
| |
| @end |