FlutterWebView.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. // Copyright 2018 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #import "FlutterWebView.h"
  5. #import "FLTWKNavigationDelegate.h"
  6. #import "JavaScriptChannelHandler.h"
  7. @implementation FLTWebViewFactory {
  8. NSObject<FlutterBinaryMessenger>* _messenger;
  9. }
  10. - (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
  11. self = [super init];
  12. if (self) {
  13. _messenger = messenger;
  14. }
  15. return self;
  16. }
  17. - (NSObject<FlutterMessageCodec>*)createArgsCodec {
  18. return [FlutterStandardMessageCodec sharedInstance];
  19. }
  20. - (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
  21. viewIdentifier:(int64_t)viewId
  22. arguments:(id _Nullable)args {
  23. FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame
  24. viewIdentifier:viewId
  25. arguments:args
  26. binaryMessenger:_messenger];
  27. return webviewController;
  28. }
  29. @end
  30. @implementation FLTWKWebView
  31. - (void)setFrame:(CGRect)frame {
  32. [super setFrame:frame];
  33. self.scrollView.contentInset = UIEdgeInsetsZero;
  34. // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of
  35. // webview's contentInsets.
  36. // self.scrollView.contentInset = UIEdgeInsetsZero;
  37. if (@available(iOS 11, *)) {
  38. // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will
  39. // always be 0.
  40. if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.adjustedContentInset, UIEdgeInsetsZero)) {
  41. return;
  42. }
  43. UIEdgeInsets insetToAdjust = self.scrollView.adjustedContentInset;
  44. self.scrollView.contentInset = UIEdgeInsetsMake(-insetToAdjust.top, -insetToAdjust.left,
  45. -insetToAdjust.bottom, -insetToAdjust.right);
  46. }
  47. }
  48. @end
  49. @implementation FLTWebViewController {
  50. FLTWKWebView* _webView;
  51. int64_t _viewId;
  52. FlutterMethodChannel* _channel;
  53. NSString* _currentUrl;
  54. // The set of registered JavaScript channel names.
  55. NSMutableSet* _javaScriptChannelNames;
  56. FLTWKNavigationDelegate* _navigationDelegate;
  57. }
  58. - (instancetype)initWithFrame:(CGRect)frame
  59. viewIdentifier:(int64_t)viewId
  60. arguments:(id _Nullable)args
  61. binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
  62. if (self = [super init]) {
  63. _viewId = viewId;
  64. NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId];
  65. _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
  66. _javaScriptChannelNames = [[NSMutableSet alloc] init];
  67. WKUserContentController* userContentController = [[WKUserContentController alloc] init];
  68. if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) {
  69. NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"];
  70. [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames];
  71. [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController];
  72. }
  73. NSDictionary<NSString*, id>* settings = args[@"settings"];
  74. WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
  75. configuration.userContentController = userContentController;
  76. [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"]
  77. inConfiguration:configuration];
  78. _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration];
  79. _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel];
  80. _webView.UIDelegate = self;
  81. _webView.navigationDelegate = _navigationDelegate;
  82. __weak __typeof__(self) weakSelf = self;
  83. [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  84. [weakSelf onMethodCall:call result:result];
  85. }];
  86. if (@available(iOS 11.0, *)) {
  87. _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  88. if (@available(iOS 13.0, *)) {
  89. _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO;
  90. }
  91. }
  92. [self applySettings:settings];
  93. // TODO(amirh): return an error if apply settings failed once it's possible to do so.
  94. // https://github.com/flutter/flutter/issues/36228
  95. NSString* initialUrl = args[@"initialUrl"];
  96. if ([initialUrl isKindOfClass:[NSString class]]) {
  97. [self loadUrl:initialUrl];
  98. }
  99. }
  100. return self;
  101. }
  102. - (UIView*)view {
  103. return _webView;
  104. }
  105. - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  106. if ([[call method] isEqualToString:@"updateSettings"]) {
  107. [self onUpdateSettings:call result:result];
  108. } else if ([[call method] isEqualToString:@"loadUrl"]) {
  109. [self onLoadUrl:call result:result];
  110. } else if ([[call method] isEqualToString:@"canGoBack"]) {
  111. [self onCanGoBack:call result:result];
  112. } else if ([[call method] isEqualToString:@"canGoForward"]) {
  113. [self onCanGoForward:call result:result];
  114. } else if ([[call method] isEqualToString:@"goBack"]) {
  115. [self onGoBack:call result:result];
  116. } else if ([[call method] isEqualToString:@"goForward"]) {
  117. [self onGoForward:call result:result];
  118. } else if ([[call method] isEqualToString:@"reload"]) {
  119. [self onReload:call result:result];
  120. } else if ([[call method] isEqualToString:@"currentUrl"]) {
  121. [self onCurrentUrl:call result:result];
  122. } else if ([[call method] isEqualToString:@"evaluateJavascript"]) {
  123. [self onEvaluateJavaScript:call result:result];
  124. } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) {
  125. [self onAddJavaScriptChannels:call result:result];
  126. } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) {
  127. [self onRemoveJavaScriptChannels:call result:result];
  128. } else if ([[call method] isEqualToString:@"clearCache"]) {
  129. [self clearCache:result];
  130. } else if ([[call method] isEqualToString:@"getTitle"]) {
  131. [self onGetTitle:result];
  132. } else {
  133. result(FlutterMethodNotImplemented);
  134. }
  135. }
  136. - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result {
  137. NSString* error = [self applySettings:[call arguments]];
  138. if (error == nil) {
  139. result(nil);
  140. return;
  141. }
  142. result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]);
  143. }
  144. - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
  145. if (![self loadRequest:[call arguments]]) {
  146. result([FlutterError
  147. errorWithCode:@"loadUrl_failed"
  148. message:@"Failed parsing the URL"
  149. details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]);
  150. } else {
  151. result(nil);
  152. }
  153. }
  154. - (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result {
  155. BOOL canGoBack = [_webView canGoBack];
  156. result([NSNumber numberWithBool:canGoBack]);
  157. }
  158. - (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result {
  159. BOOL canGoForward = [_webView canGoForward];
  160. result([NSNumber numberWithBool:canGoForward]);
  161. }
  162. - (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result {
  163. [_webView goBack];
  164. result(nil);
  165. }
  166. - (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result {
  167. [_webView goForward];
  168. result(nil);
  169. }
  170. - (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result {
  171. [_webView reload];
  172. result(nil);
  173. }
  174. - (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
  175. _currentUrl = [[_webView URL] absoluteString];
  176. result(_currentUrl);
  177. }
  178. - (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result {
  179. NSString* jsString = [call arguments];
  180. if (!jsString) {
  181. result([FlutterError errorWithCode:@"evaluateJavaScript_failed"
  182. message:@"JavaScript String cannot be null"
  183. details:nil]);
  184. return;
  185. }
  186. [_webView evaluateJavaScript:jsString
  187. completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) {
  188. if (error) {
  189. result([FlutterError
  190. errorWithCode:@"evaluateJavaScript_failed"
  191. message:@"Failed evaluating JavaScript"
  192. details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@",
  193. jsString, error]]);
  194. } else {
  195. result([NSString stringWithFormat:@"%@", evaluateResult]);
  196. }
  197. }];
  198. }
  199. - (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result {
  200. NSArray* channelNames = [call arguments];
  201. NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames];
  202. [_javaScriptChannelNames addObjectsFromArray:channelNames];
  203. [self registerJavaScriptChannels:channelNamesSet
  204. controller:_webView.configuration.userContentController];
  205. result(nil);
  206. }
  207. - (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result {
  208. // WkWebView does not support removing a single user script, so instead we remove all
  209. // user scripts, all message handlers. And re-register channels that shouldn't be removed.
  210. [_webView.configuration.userContentController removeAllUserScripts];
  211. for (NSString* channelName in _javaScriptChannelNames) {
  212. [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName];
  213. }
  214. NSArray* channelNamesToRemove = [call arguments];
  215. for (NSString* channelName in channelNamesToRemove) {
  216. [_javaScriptChannelNames removeObject:channelName];
  217. }
  218. [self registerJavaScriptChannels:_javaScriptChannelNames
  219. controller:_webView.configuration.userContentController];
  220. result(nil);
  221. }
  222. - (void)clearCache:(FlutterResult)result {
  223. if (@available(iOS 9.0, *)) {
  224. NSSet* cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
  225. WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore];
  226. NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
  227. [dataStore removeDataOfTypes:cacheDataTypes
  228. modifiedSince:dateFrom
  229. completionHandler:^{
  230. result(nil);
  231. }];
  232. } else {
  233. // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624.
  234. NSLog(@"Clearing cache is not supported for Flutter WebViews prior to iOS 9.");
  235. }
  236. }
  237. - (void)onGetTitle:(FlutterResult)result {
  238. NSString* title = _webView.title;
  239. result(title);
  240. }
  241. // Returns nil when successful, or an error message when one or more keys are unknown.
  242. - (NSString*)applySettings:(NSDictionary<NSString*, id>*)settings {
  243. NSMutableArray<NSString*>* unknownKeys = [[NSMutableArray alloc] init];
  244. for (NSString* key in settings) {
  245. if ([key isEqualToString:@"jsMode"]) {
  246. NSNumber* mode = settings[key];
  247. [self updateJsMode:mode];
  248. } else if ([key isEqualToString:@"hasNavigationDelegate"]) {
  249. NSNumber* hasDartNavigationDelegate = settings[key];
  250. _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue];
  251. } else if ([key isEqualToString:@"debuggingEnabled"]) {
  252. // no-op debugging is always enabled on iOS.
  253. } else if ([key isEqualToString:@"gestureNavigationEnabled"]) {
  254. NSNumber* allowsBackForwardNavigationGestures = settings[key];
  255. _webView.allowsBackForwardNavigationGestures =
  256. [allowsBackForwardNavigationGestures boolValue];
  257. } else if ([key isEqualToString:@"userAgent"]) {
  258. NSString* userAgent = settings[key];
  259. [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent];
  260. } else {
  261. [unknownKeys addObject:key];
  262. }
  263. }
  264. if ([unknownKeys count] == 0) {
  265. return nil;
  266. }
  267. return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}",
  268. [unknownKeys componentsJoinedByString:@", "]];
  269. }
  270. - (void)updateJsMode:(NSNumber*)mode {
  271. WKPreferences* preferences = [[_webView configuration] preferences];
  272. switch ([mode integerValue]) {
  273. case 0: // disabled
  274. [preferences setJavaScriptEnabled:NO];
  275. break;
  276. case 1: // unrestricted
  277. [preferences setJavaScriptEnabled:YES];
  278. break;
  279. default:
  280. NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode);
  281. }
  282. }
  283. - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy
  284. inConfiguration:(WKWebViewConfiguration*)configuration {
  285. switch ([policy integerValue]) {
  286. case 0: // require_user_action_for_all_media_types
  287. if (@available(iOS 10.0, *)) {
  288. configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll;
  289. } else {
  290. configuration.mediaPlaybackRequiresUserAction = true;
  291. }
  292. break;
  293. case 1: // always_allow
  294. if (@available(iOS 10.0, *)) {
  295. configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
  296. } else {
  297. configuration.mediaPlaybackRequiresUserAction = false;
  298. }
  299. break;
  300. default:
  301. NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy);
  302. }
  303. }
  304. - (bool)loadRequest:(NSDictionary<NSString*, id>*)request {
  305. if (!request) {
  306. return false;
  307. }
  308. NSString* url = request[@"url"];
  309. if ([url isKindOfClass:[NSString class]]) {
  310. id headers = request[@"headers"];
  311. if ([headers isKindOfClass:[NSDictionary class]]) {
  312. return [self loadUrl:url withHeaders:headers];
  313. } else {
  314. return [self loadUrl:url];
  315. }
  316. }
  317. return false;
  318. }
  319. - (bool)loadUrl:(NSString*)url {
  320. return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]];
  321. }
  322. - (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {
  323. NSURL* nsUrl = [NSURL URLWithString:url];
  324. if (!nsUrl) {
  325. return false;
  326. }
  327. NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
  328. [request setAllHTTPHeaderFields:headers];
  329. [_webView loadRequest:request];
  330. return true;
  331. }
  332. - (void)registerJavaScriptChannels:(NSSet*)channelNames
  333. controller:(WKUserContentController*)userContentController {
  334. for (NSString* channelName in channelNames) {
  335. FLTJavaScriptChannel* channel =
  336. [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel
  337. javaScriptChannelName:channelName];
  338. [userContentController addScriptMessageHandler:channel name:channelName];
  339. NSString* wrapperSource = [NSString
  340. stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName];
  341. WKUserScript* wrapperScript =
  342. [[WKUserScript alloc] initWithSource:wrapperSource
  343. injectionTime:WKUserScriptInjectionTimeAtDocumentStart
  344. forMainFrameOnly:NO];
  345. [userContentController addUserScript:wrapperScript];
  346. }
  347. }
  348. - (void)updateUserAgent:(NSString*)userAgent {
  349. if (@available(iOS 9.0, *)) {
  350. [_webView setCustomUserAgent:userAgent];
  351. } else {
  352. NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9.");
  353. }
  354. }
  355. #pragma mark WKUIDelegate
  356. - (WKWebView*)webView:(WKWebView*)webView
  357. createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration
  358. forNavigationAction:(WKNavigationAction*)navigationAction
  359. windowFeatures:(WKWindowFeatures*)windowFeatures {
  360. if (!navigationAction.targetFrame.isMainFrame) {
  361. [webView loadRequest:navigationAction.request];
  362. }
  363. return nil;
  364. }
  365. @end