#import "FlutterWebviewPlugin.h" #import "JavaScriptChannelHandler.h" static NSString *const CHANNEL_NAME = @"flutter_webview_plugin"; // UIWebViewDelegate @interface FlutterWebviewPlugin() { BOOL _enableAppScheme; BOOL _enableZoom; NSString* _invalidUrlRegex; NSMutableSet* _javaScriptChannelNames; } @end @implementation FlutterWebviewPlugin + (void)registerWithRegistrar:(NSObject*)registrar { channel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME binaryMessenger:[registrar messenger]]; UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController; FlutterWebviewPlugin* instance = [[FlutterWebviewPlugin alloc] initWithViewController:viewController]; [registrar addMethodCallDelegate:instance channel:channel]; } - (instancetype)initWithViewController:(UIViewController *)viewController { self = [super init]; if (self) { self.viewController = viewController; } return self; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@"launch" isEqualToString:call.method]) { if (!self.webview) [self initWebview:call]; else [self navigate:call]; result(nil); } else if ([@"close" isEqualToString:call.method]) { [self closeWebView]; result(nil); } else if ([@"eval" isEqualToString:call.method]) { [self evalJavascript:call completionHandler:^(NSString * response) { result(response); }]; } else if ([@"resize" isEqualToString:call.method]) { [self resize:call]; result(nil); } else if ([@"reloadUrl" isEqualToString:call.method]) { [self reloadUrl:call]; result(nil); } else if ([@"show" isEqualToString:call.method]) { [self show]; result(nil); } else if ([@"hide" isEqualToString:call.method]) { [self hide]; result(nil); } else if ([@"stopLoading" isEqualToString:call.method]) { [self stopLoading]; result(nil); } else if ([@"cleanCookies" isEqualToString:call.method]) { [self cleanCookies]; } else if ([@"back" isEqualToString:call.method]) { [self back]; result(nil); } else if ([@"forward" isEqualToString:call.method]) { [self forward]; result(nil); } else if ([@"reload" isEqualToString:call.method]) { [self reload]; result(nil); } else { result(FlutterMethodNotImplemented); } } - (void)initWebview:(FlutterMethodCall*)call { NSNumber *clearCache = call.arguments[@"clearCache"]; NSNumber *clearCookies = call.arguments[@"clearCookies"]; NSNumber *hidden = call.arguments[@"hidden"]; NSDictionary *rect = call.arguments[@"rect"]; _enableAppScheme = call.arguments[@"enableAppScheme"]; NSString *userAgent = call.arguments[@"userAgent"]; NSNumber *withZoom = call.arguments[@"withZoom"]; NSNumber *scrollBar = call.arguments[@"scrollBar"]; NSNumber *withJavascript = call.arguments[@"withJavascript"]; _invalidUrlRegex = call.arguments[@"invalidUrlRegex"]; _javaScriptChannelNames = [[NSMutableSet alloc] init]; WKUserContentController* userContentController = [[WKUserContentController alloc] init]; if ([call.arguments[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { NSArray* javaScriptChannelNames = call.arguments[@"javascriptChannelNames"]; [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; } if (clearCache != (id)[NSNull null] && [clearCache boolValue]) { [[NSURLCache sharedURLCache] removeAllCachedResponses]; } if (clearCookies != (id)[NSNull null] && [clearCookies boolValue]) { if (@available(iOS 9.0, *)) { NSSet *websiteDataTypes = [NSSet setWithArray:@[ WKWebsiteDataTypeCookies, ]]; NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{ }]; } else { // Fallback on earlier versions } } if (userAgent != (id)[NSNull null]) { [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": userAgent}]; } CGRect rc; if (rect != nil) { rc = [self parseRect:rect]; } else { rc = self.viewController.view.bounds; } WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; configuration.userContentController = userContentController; self.webview = [[WKWebView alloc] initWithFrame:rc configuration:configuration]; self.webview.UIDelegate = self; self.webview.navigationDelegate = self; self.webview.scrollView.delegate = self; self.webview.hidden = [hidden boolValue]; self.webview.scrollView.showsHorizontalScrollIndicator = [scrollBar boolValue]; self.webview.scrollView.showsVerticalScrollIndicator = [scrollBar boolValue]; [self.webview addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL]; WKPreferences* preferences = [[self.webview configuration] preferences]; if ([withJavascript boolValue]) { [preferences setJavaScriptEnabled:YES]; } else { [preferences setJavaScriptEnabled:NO]; } _enableZoom = [withZoom boolValue]; UIViewController* presentedViewController = self.viewController.presentedViewController; UIViewController* currentViewController = presentedViewController != nil ? presentedViewController : self.viewController; [currentViewController.view addSubview:self.webview]; [self navigate:call]; } - (CGRect)parseRect:(NSDictionary *)rect { return CGRectMake([[rect valueForKey:@"left"] doubleValue], [[rect valueForKey:@"top"] doubleValue], [[rect valueForKey:@"width"] doubleValue], [[rect valueForKey:@"height"] doubleValue]); } - (void) scrollViewDidScroll:(UIScrollView *)scrollView { id xDirection = @{@"xDirection": @(scrollView.contentOffset.x) }; [channel invokeMethod:@"onScrollXChanged" arguments:xDirection]; id yDirection = @{@"yDirection": @(scrollView.contentOffset.y) }; [channel invokeMethod:@"onScrollYChanged" arguments:yDirection]; } - (void)navigate:(FlutterMethodCall*)call { if (self.webview != nil) { NSString *url = call.arguments[@"url"]; NSNumber *withLocalUrl = call.arguments[@"withLocalUrl"]; if ( [withLocalUrl boolValue]) { NSURL *htmlUrl = [NSURL fileURLWithPath:url isDirectory:false]; NSString *localUrlScope = call.arguments[@"localUrlScope"]; if (@available(iOS 9.0, *)) { if(localUrlScope == nil) { [self.webview loadFileURL:htmlUrl allowingReadAccessToURL:htmlUrl]; } else { NSURL *scopeUrl = [NSURL fileURLWithPath:localUrlScope]; [self.webview loadFileURL:htmlUrl allowingReadAccessToURL:scopeUrl]; } } else { @throw @"not available on version earlier than ios 9.0"; } } else { NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; NSDictionary *headers = call.arguments[@"headers"]; if (headers != nil) { [request setAllHTTPHeaderFields:headers]; } [self.webview loadRequest:request]; } } } - (void)evalJavascript:(FlutterMethodCall*)call completionHandler:(void (^_Nullable)(NSString * response))completionHandler { if (self.webview != nil) { NSString *code = call.arguments[@"code"]; [self.webview evaluateJavaScript:code completionHandler:^(id _Nullable response, NSError * _Nullable error) { completionHandler([NSString stringWithFormat:@"%@", response]); }]; } else { completionHandler(nil); } } - (void)resize:(FlutterMethodCall*)call { if (self.webview != nil) { NSDictionary *rect = call.arguments[@"rect"]; CGRect rc = [self parseRect:rect]; self.webview.frame = rc; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"estimatedProgress"] && object == self.webview) { [channel invokeMethod:@"onProgressChanged" arguments:@{@"progress": @(self.webview.estimatedProgress)}]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)closeWebView { if (self.webview != nil) { [self.webview stopLoading]; [self.webview removeFromSuperview]; self.webview.navigationDelegate = nil; [self.webview removeObserver:self forKeyPath:@"estimatedProgress"]; self.webview = nil; // manually trigger onDestroy [channel invokeMethod:@"onDestroy" arguments:nil]; } } - (void)reloadUrl:(FlutterMethodCall*)call { if (self.webview != nil) { NSString *url = call.arguments[@"url"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; NSDictionary *headers = call.arguments[@"headers"]; if (headers != nil) { [request setAllHTTPHeaderFields:headers]; } [self.webview loadRequest:request]; } } - (void)show { if (self.webview != nil) { self.webview.hidden = false; } } - (void)hide { if (self.webview != nil) { self.webview.hidden = true; } } - (void)stopLoading { if (self.webview != nil) { [self.webview stopLoading]; } } - (void)back { if (self.webview != nil) { [self.webview goBack]; } } - (void)forward { if (self.webview != nil) { [self.webview goForward]; } } - (void)reload { if (self.webview != nil) { [self.webview reload]; } } - (void)cleanCookies { [[NSURLSession sharedSession] resetWithCompletionHandler:^{ }]; } - (bool)checkInvalidUrl:(NSURL*)url { NSString* urlString = url != nil ? [url absoluteString] : nil; if (_invalidUrlRegex != [NSNull null] && urlString != nil) { NSError* error = NULL; NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:_invalidUrlRegex options:NSRegularExpressionCaseInsensitive error:&error]; NSTextCheckingResult* match = [regex firstMatchInString:urlString options:0 range:NSMakeRange(0, [urlString length])]; return match != nil; } else { return false; } } #pragma mark -- WkWebView Delegate - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { BOOL isInvalid = [self checkInvalidUrl: navigationAction.request.URL]; id data = @{@"url": navigationAction.request.URL.absoluteString, @"type": isInvalid ? @"abortLoad" : @"shouldStart", @"navigationType": [NSNumber numberWithInt:navigationAction.navigationType]}; [channel invokeMethod:@"onState" arguments:data]; if (navigationAction.navigationType == WKNavigationTypeBackForward) { [channel invokeMethod:@"onBackPressed" arguments:nil]; } else if (!isInvalid) { id data = @{@"url": navigationAction.request.URL.absoluteString}; [channel invokeMethod:@"onUrlChanged" arguments:data]; } if (_enableAppScheme || ([webView.URL.scheme isEqualToString:@"http"] || [webView.URL.scheme isEqualToString:@"https"] || [webView.URL.scheme isEqualToString:@"about"] || [webView.URL.scheme isEqualToString:@"file"])) { if (isInvalid) { decisionHandler(WKNavigationActionPolicyCancel); } else { decisionHandler(WKNavigationActionPolicyAllow); } } else { decisionHandler(WKNavigationActionPolicyCancel); } } - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { if (!navigationAction.targetFrame.isMainFrame) { [webView loadRequest:navigationAction.request]; } return nil; } - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { [channel invokeMethod:@"onState" arguments:@{@"type": @"startLoad", @"url": webView.URL.absoluteString}]; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { [channel invokeMethod:@"onState" arguments:@{@"type": @"finishLoad", @"url": webView.URL.absoluteString}]; } - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { [channel invokeMethod:@"onError" arguments:@{@"code": [NSString stringWithFormat:@"%ld", error.code], @"error": error.localizedDescription}]; } - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; [channel invokeMethod:@"onHttpError" arguments:@{@"code": [NSString stringWithFormat:@"%ld", response.statusCode], @"url": webView.URL.absoluteString}]; } decisionHandler(WKNavigationResponsePolicyAllow); } - (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]; } } #pragma mark -- UIScrollViewDelegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { if (scrollView.pinchGestureRecognizer.isEnabled != _enableZoom) { scrollView.pinchGestureRecognizer.enabled = _enableZoom; } } @end