| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- // Copyright 2018 The Chromium 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 "JavaScriptChannelHandler.h"
- @implementation FLTWebViewFactory {
- NSObject<FlutterBinaryMessenger>* _messenger;
- }
- - (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
- self = [super init];
- if (self) {
- _messenger = messenger;
- }
- return self;
- }
- - (NSObject<FlutterMessageCodec>*)createArgsCodec {
- return [FlutterStandardMessageCodec sharedInstance];
- }
- - (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
- viewIdentifier:(int64_t)viewId
- arguments:(id _Nullable)args {
- 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;
- }
- - (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];
- configuration.userContentController = userContentController;
- [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"]
- inConfiguration:configuration];
- _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration];
- _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]]) {
- [self loadUrl:initialUrl];
- }
- }
- return self;
- }
- - (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:@"loadUrl"]) {
- [self onLoadUrl: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:@"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 {
- 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)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
- if (![self loadRequest:[call arguments]]) {
- result([FlutterError
- errorWithCode:@"loadUrl_failed"
- message:@"Failed parsing the URL"
- details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]);
- } else {
- result(nil);
- }
- }
- - (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result {
- BOOL canGoBack = [_webView canGoBack];
- result([NSNumber numberWithBool:canGoBack]);
- }
- - (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result {
- BOOL canGoForward = [_webView canGoForward];
- result([NSNumber numberWithBool: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)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 {
- if (@available(iOS 9.0, *)) {
- NSSet* cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
- WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore];
- NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
- [dataStore removeDataOfTypes:cacheDataTypes
- modifiedSince:dateFrom
- completionHandler:^{
- result(nil);
- }];
- } else {
- // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624.
- NSLog(@"Clearing cache is not supported for Flutter WebViews prior to iOS 9.");
- }
- }
- - (void)onGetTitle:(FlutterResult)result {
- NSString* title = _webView.title;
- result(title);
- }
- // 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:@"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 {
- [unknownKeys addObject:key];
- }
- }
- if ([unknownKeys count] == 0) {
- return nil;
- }
- return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}",
- [unknownKeys componentsJoinedByString:@", "]];
- }
- - (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 {
- configuration.mediaPlaybackRequiresUserAction = true;
- }
- break;
- case 1: // always_allow
- if (@available(iOS 10.0, *)) {
- configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
- } else {
- configuration.mediaPlaybackRequiresUserAction = false;
- }
- break;
- default:
- NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy);
- }
- }
- - (bool)loadRequest:(NSDictionary<NSString*, id>*)request {
- if (!request) {
- return false;
- }
- NSString* url = request[@"url"];
- if ([url isKindOfClass:[NSString class]]) {
- id headers = request[@"headers"];
- if ([headers isKindOfClass:[NSDictionary class]]) {
- return [self loadUrl:url withHeaders:headers];
- } else {
- return [self loadUrl:url];
- }
- }
- return false;
- }
- - (bool)loadUrl:(NSString*)url {
- return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]];
- }
- - (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {
- NSURL* nsUrl = [NSURL URLWithString:url];
- if (!nsUrl) {
- return false;
- }
- NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
- [request setAllHTTPHeaderFields:headers];
- [_webView loadRequest:request];
- return true;
- }
- - (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 {
- if (@available(iOS 9.0, *)) {
- [_webView setCustomUserAgent:userAgent];
- } else {
- NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9.");
- }
- }
- #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
|