[webview_flutter] WKWebView implementation of loadFlutterAsset method. (#4582)

diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md
index c1d2712..0aaf4bc 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.7.0
+
+* Adds implementation of the `loadFlutterAsset` method from the platform interface.
+
 ## 2.6.0
 
 * Implements new cookie manager for setting cookies and providing initial cookies.
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html
new file mode 100644
index 0000000..9895dd3
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html lang="en">
+<head>
+<title>Load file or HTML string example</title>
+<link rel="stylesheet" href="styles/style.css" />
+</head>
+<body>
+
+<h1>Local demo page</h1>
+<p>
+  This is an example page used to demonstrate how to load a local file or HTML 
+  string using the <a href="https://pub.dev/packages/webview_flutter">Flutter 
+  webview</a> plugin.
+</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css
new file mode 100644
index 0000000..c2140b8
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css
@@ -0,0 +1,3 @@
+h1 {
+    color: blue;
+}
\ No newline at end of file
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
index 6c7c6bf..976b9c4 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
@@ -164,7 +164,120 @@
   OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]);
 }
 
-- (void)testLoadFileSucceedsWithBaseUrl {
+- (void)testLoadFlutterAssetSucceeds {
+  NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]);
+  NSString *filePath = [FlutterDartProject lookupKeyForAsset:@"assets/file.html"];
+  NSURL *url = [NSURL URLWithString:[@"file:///" stringByAppendingString:filePath]];
+  [OCMStub([mockBundle URLForResource:[filePath stringByDeletingPathExtension]
+                        withExtension:@"html"]) andReturn:(url)];
+
+  XCTestExpectation *resultExpectation =
+      [self expectationWithDescription:@"Should return successful result over the method channel."];
+  FLTWebViewController *controller =
+      [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+                                   viewIdentifier:1
+                                        arguments:nil
+                                  binaryMessenger:self.mockBinaryMessenger];
+  FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+  controller.webView = mockWebView;
+  [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset"
+                                                             arguments:@"assets/file.html"]
+                    result:^(id _Nullable result) {
+                      XCTAssertNil(result);
+                      [resultExpectation fulfill];
+                    }];
+
+  [self waitForExpectations:@[ resultExpectation ] timeout:1.0];
+  OCMVerify([mockWebView loadFileURL:url
+             allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]);
+}
+
+- (void)testLoadFlutterAssetFailsWithInvalidKey {
+  NSArray *resultExpectations = @[
+    [self expectationWithDescription:@"Should return failed result when argument is nil."],
+    [self expectationWithDescription:
+              @"Should return failed result when argument is not of type NSString*."],
+    [self expectationWithDescription:
+              @"Should return failed result when argument is an empty string."],
+  ];
+
+  FLTWebViewController *controller =
+      [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+                                   viewIdentifier:1
+                                        arguments:nil
+                                  binaryMessenger:self.mockBinaryMessenger];
+  FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+  controller.webView = mockWebView;
+  [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset"
+                                                             arguments:nil]
+                    result:^(id _Nullable result) {
+                      FlutterError *expected =
+                          [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey"
+                                              message:@"Supplied asset key is not valid."
+                                              details:@"Argument is nil."];
+                      [FLTWebViewTests assertFlutterError:result withExpected:expected];
+                      [resultExpectations[0] fulfill];
+                    }];
+  [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset"
+                                                             arguments:@(10)]
+                    result:^(id _Nullable result) {
+                      FlutterError *expected =
+                          [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey"
+                                              message:@"Supplied asset key is not valid."
+                                              details:@"Argument is not of type NSString."];
+                      [FLTWebViewTests assertFlutterError:result withExpected:expected];
+                      [resultExpectations[1] fulfill];
+                    }];
+  [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset"
+                                                             arguments:@""]
+                    result:^(id _Nullable result) {
+                      FlutterError *expected =
+                          [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey"
+                                              message:@"Supplied asset key is not valid."
+                                              details:@"Argument contains an empty string."];
+                      [FLTWebViewTests assertFlutterError:result withExpected:expected];
+                      [resultExpectations[2] fulfill];
+                    }];
+
+  [self waitForExpectations:resultExpectations timeout:1.0];
+  OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]);
+}
+
+- (void)testLoadFlutterAssetFailsWithParsingError {
+  NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]);
+  NSString *filePath = [FlutterDartProject lookupKeyForAsset:@"assets/file.html"];
+  [OCMStub([mockBundle URLForResource:[filePath stringByDeletingPathExtension]
+                        withExtension:@"html"]) andReturn:(nil)];
+
+  XCTestExpectation *resultExpectation =
+      [self expectationWithDescription:@"Should return failed result over the method channel."];
+  FLTWebViewController *controller =
+      [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+                                   viewIdentifier:1
+                                        arguments:nil
+                                  binaryMessenger:self.mockBinaryMessenger];
+  FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+  controller.webView = mockWebView;
+  [controller
+      onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset"
+                                                     arguments:@"assets/file.html"]
+            result:^(id _Nullable result) {
+              FlutterError *expected = [FlutterError
+                  errorWithCode:@"loadFlutterAsset_invalidKey"
+                        message:@"Failed parsing file path for supplied key."
+                        details:[NSString
+                                    stringWithFormat:
+                                        @"Failed to convert path '%@' into NSURL for key '%@'.",
+                                        filePath, @"assets/file.html"]];
+              [FLTWebViewTests assertFlutterError:result withExpected:expected];
+              [resultExpectation fulfill];
+            }];
+
+  [self waitForExpectations:@[ resultExpectation ] timeout:1.0];
+  OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]);
+}
+
+- (void)testLoadHtmlStringSucceedsWithBaseUrl {
   NSURL *baseUrl = [NSURL URLWithString:@"https://flutter.dev"];
   XCTestExpectation *resultExpectation =
       [self expectationWithDescription:@"Should return successful result over the method channel."];
@@ -189,7 +302,7 @@
   OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:baseUrl]);
 }
 
-- (void)testLoadFileSucceedsWithoutBaseUrl {
+- (void)testLoadHtmlStringSucceedsWithoutBaseUrl {
   XCTestExpectation *resultExpectation =
       [self expectationWithDescription:@"Should return successful result over the method channel."];
   FLTWebViewController *controller =
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart
index ce7548a..d4e0ec4 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart
@@ -171,6 +171,7 @@
   listCache,
   clearCache,
   navigationDelegate,
+  loadFlutterAsset,
   loadLocalFile,
   loadHtmlString,
   doPostRequest,
@@ -214,6 +215,9 @@
               case _MenuOptions.navigationDelegate:
                 _onNavigationDelegateExample(controller.data!, context);
                 break;
+              case _MenuOptions.loadFlutterAsset:
+                _onLoadFlutterAssetExample(controller.data!, context);
+                break;
               case _MenuOptions.loadLocalFile:
                 _onLoadLocalFileExample(controller.data!, context);
                 break;
@@ -262,6 +266,10 @@
               child: Text('Navigation Delegate example'),
             ),
             const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.loadFlutterAsset,
+              child: Text('Load Flutter Asset'),
+            ),
+            const PopupMenuItem<_MenuOptions>(
               value: _MenuOptions.loadHtmlString,
               child: Text('Load HTML string'),
             ),
@@ -355,6 +363,11 @@
     await controller.loadUrl('data:text/html;base64,$contentBase64');
   }
 
+  Future<void> _onLoadFlutterAssetExample(
+      WebViewController controller, BuildContext context) async {
+    await controller.loadFlutterAsset('assets/www/index.html');
+  }
+
   Future<void> _onLoadLocalFileExample(
       WebViewController controller, BuildContext context) async {
     final String pathToIndex = await _prepareLocalFile();
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart
index 1efd031..4d479f9 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart
@@ -319,6 +319,14 @@
 
   WebView _widget;
 
+  /// Loads the Flutter asset specified in the pubspec.yaml file.
+  ///
+  /// Throws an ArgumentError if [key] is not part of the specified assets
+  /// in the pubspec.yaml file.
+  Future<void> loadFlutterAsset(String key) {
+    return _webViewPlatformController.loadFlutterAsset(key);
+  }
+
   /// Loads the file located on the specified [absoluteFilePath].
   ///
   /// The [absoluteFilePath] parameter should contain the absolute path to the
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml
index 2070b11..b8c2464 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml
@@ -34,3 +34,5 @@
   assets:
     - assets/sample_audio.ogg
     - assets/sample_video.mp4
+    - assets/www/index.html
+    - assets/www/styles/style.css
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
index ea45a8b..3f3b9d9 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
+++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
@@ -163,6 +163,8 @@
     [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"]) {
@@ -244,6 +246,34 @@
   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]) {
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml
index fc274b4..08e98b1 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
 repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
-version: 2.6.0
+version: 2.7.0
 
 environment:
   sdk: ">=2.14.0 <3.0.0"