[e2e] Creates basic support/documentation/example to iOS. (#2394)

* * Updates E2EPlugin and add skeleton iOS test case E2EIosTest.
* Adds instructions to README.md about e2e testing on iOS device.
* Applies iOS e2e to example.

Co-Authored-By: Collin Jackson <jackson@google.com>
diff --git a/packages/e2e/CHANGELOG.md b/packages/e2e/CHANGELOG.md
index 1b09c89..083e4d8 100644
--- a/packages/e2e/CHANGELOG.md
+++ b/packages/e2e/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.2.3
+
+* Updates `E2EPlugin` and add skeleton iOS test case `E2EIosTest`.
+* Adds instructions to README.md about e2e testing on iOS devices.
+* Adds iOS e2e testing to example.
+
 ## 0.2.2+3
 
 * Remove the deprecated `author:` field from pubspec.yaml
diff --git a/packages/e2e/README.md b/packages/e2e/README.md
index bafbff1..b3c19ec 100644
--- a/packages/e2e/README.md
+++ b/packages/e2e/README.md
@@ -151,4 +151,33 @@
 devices you want to test on. See
 [gcloud firebase test android run](https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run).
 
-iOS support for Firebase Test Lab is not yet available, but is planned.
+## iOS device testing
+
+You need to change `iOS/Podfile` to avoid test target statically linking to the plugins. One way is to
+link all of the plugins dynamically:
+
+```
+target 'Runner' do
+  use_frameworks!
+  ...
+end
+```
+
+To e2e test on your iOS device (simulator or real), rebuild your iOS targets with Flutter tool.
+
+```
+flutter build ios -t test_driver/<package_name>_e2e.dart (--simulator)
+```
+
+Open Xcode project (by default, it's `ios/Runner.xcodeproj`). Create a test target
+(navigating `File > New > Target...` and set up the values) and a test file `RunnerTests.m` and
+change the code. You can change `RunnerTests.m` to the name of your choice.
+
+```objective-c
+#import <XCTest/XCTest.h>
+#import <e2e/E2EIosTest.h>
+
+E2E_IOS_RUNNER(RunnerTests)
+```
+
+Now you can start RunnerTests to kick out e2e tests!
diff --git a/packages/e2e/example/ios/Runner.xcodeproj/project.pbxproj b/packages/e2e/example/ios/Runner.xcodeproj/project.pbxproj
index 88aca9f..b96fa2f 100644
--- a/packages/e2e/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/e2e/example/ios/Runner.xcodeproj/project.pbxproj
@@ -11,6 +11,7 @@
 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
 		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
 		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		769541CB23A0351900E5C350 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 769541CA23A0351900E5C350 /* RunnerTests.m */; };
 		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
 		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
@@ -18,9 +19,19 @@
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-		F81AEF02CE63DA0020B29F57 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 241E53603CE376E3BCB194D3 /* libPods-Runner.a */; };
+		C2A5EDF11F4FDBF3ABFD7006 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */; };
 /* End PBXBuildFile section */
 
+/* Begin PBXContainerItemProxy section */
+		769541CD23A0351900E5C350 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+			remoteInfo = Runner;
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXCopyFilesBuildPhase section */
 		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
 			isa = PBXCopyFilesBuildPhase;
@@ -40,9 +51,13 @@
 		0D6F1CB5DBBEBCC75AFAD041 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
-		241E53603CE376E3BCB194D3 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
 		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
+		625A5A90428602E25C0DE2F6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		769541BF23A0337200E5C350 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
+		769541C823A0351900E5C350 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		769541CA23A0351900E5C350 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
+		769541CC23A0351900E5C350 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
 		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
 		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@@ -60,13 +75,20 @@
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+		769541C523A0351900E5C350 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		97C146EB1CF9000F007C117D /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
 				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
 				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
-				F81AEF02CE63DA0020B29F57 /* libPods-Runner.a in Frameworks */,
+				C2A5EDF11F4FDBF3ABFD7006 /* libPods-Runner.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -76,11 +98,21 @@
 		42D734D13B733A64B01A24A9 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
-				241E53603CE376E3BCB194D3 /* libPods-Runner.a */,
+				769541BF23A0337200E5C350 /* XCTest.framework */,
+				625A5A90428602E25C0DE2F6 /* libPods-Runner.a */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		769541C923A0351900E5C350 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				769541CA23A0351900E5C350 /* RunnerTests.m */,
+				769541CC23A0351900E5C350 /* Info.plist */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
 		9740EEB11CF90186004384FC /* Flutter */ = {
 			isa = PBXGroup;
 			children = (
@@ -99,6 +131,7 @@
 			children = (
 				9740EEB11CF90186004384FC /* Flutter */,
 				97C146F01CF9000F007C117D /* Runner */,
+				769541C923A0351900E5C350 /* RunnerTests */,
 				97C146EF1CF9000F007C117D /* Products */,
 				BAB55133DD7BD81A2557E916 /* Pods */,
 				42D734D13B733A64B01A24A9 /* Frameworks */,
@@ -109,6 +142,7 @@
 			isa = PBXGroup;
 			children = (
 				97C146EE1CF9000F007C117D /* Runner.app */,
+				769541C823A0351900E5C350 /* RunnerTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -144,13 +178,30 @@
 				0D6F1CB5DBBEBCC75AFAD041 /* Pods-Runner.release.xcconfig */,
 				E23EF4D45DAE46B9DDB9B445 /* Pods-Runner.profile.xcconfig */,
 			);
-			name = Pods;
 			path = Pods;
 			sourceTree = "<group>";
 		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
+		769541C723A0351900E5C350 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 769541CF23A0351900E5C350 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				769541C423A0351900E5C350 /* Sources */,
+				769541C523A0351900E5C350 /* Frameworks */,
+				769541C623A0351900E5C350 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				769541CE23A0351900E5C350 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 769541C823A0351900E5C350 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 		97C146ED1CF9000F007C117D /* Runner */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -182,6 +233,11 @@
 				LastUpgradeCheck = 1020;
 				ORGANIZATIONNAME = "The Chromium Authors";
 				TargetAttributes = {
+					769541C723A0351900E5C350 = {
+						CreatedOnToolsVersion = 11.0;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 97C146ED1CF9000F007C117D;
+					};
 					97C146ED1CF9000F007C117D = {
 						CreatedOnToolsVersion = 7.3.1;
 					};
@@ -201,11 +257,19 @@
 			projectRoot = "";
 			targets = (
 				97C146ED1CF9000F007C117D /* Runner */,
+				769541C723A0351900E5C350 /* RunnerTests */,
 			);
 		};
 /* End PBXProject section */
 
 /* Begin PBXResourcesBuildPhase section */
+		769541C623A0351900E5C350 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		97C146EC1CF9000F007C117D /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -288,6 +352,14 @@
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
+		769541C423A0351900E5C350 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				769541CB23A0351900E5C350 /* RunnerTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		97C146EA1CF9000F007C117D /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -300,6 +372,14 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		769541CE23A0351900E5C350 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 97C146ED1CF9000F007C117D /* Runner */;
+			targetProxy = 769541CD23A0351900E5C350 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin PBXVariantGroup section */
 		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
 			isa = PBXVariantGroup;
@@ -394,6 +474,73 @@
 			};
 			name = Profile;
 		};
+		769541D023A0351900E5C350 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Debug;
+		};
+		769541D123A0351900E5C350 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Release;
+		};
+		769541D223A0351900E5C350 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Profile;
+		};
 		97C147031CF9000F007C117D /* Debug */ = {
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
@@ -550,6 +697,16 @@
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
+		769541CF23A0351900E5C350 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				769541D023A0351900E5C350 /* Debug */,
+				769541D123A0351900E5C350 /* Release */,
+				769541D223A0351900E5C350 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
diff --git a/packages/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index a28140c..72fa146 100644
--- a/packages/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -27,8 +27,6 @@
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-      </Testables>
       <MacroExpansion>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -38,8 +36,18 @@
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "769541C723A0351900E5C350"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -61,8 +69,6 @@
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Profile"
diff --git a/packages/e2e/example/ios/RunnerTests/Info.plist b/packages/e2e/example/ios/RunnerTests/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/e2e/example/ios/RunnerTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/packages/e2e/example/ios/RunnerTests/RunnerTests.m b/packages/e2e/example/ios/RunnerTests/RunnerTests.m
new file mode 100644
index 0000000..9614c65
--- /dev/null
+++ b/packages/e2e/example/ios/RunnerTests/RunnerTests.m
@@ -0,0 +1,4 @@
+#import <XCTest/XCTest.h>
+#import <e2e/E2EIosTest.h>
+
+E2E_IOS_RUNNER(RunnerTests)
diff --git a/packages/e2e/ios/Classes/E2EIosTest.h b/packages/e2e/ios/Classes/E2EIosTest.h
new file mode 100644
index 0000000..1d76514
--- /dev/null
+++ b/packages/e2e/ios/Classes/E2EIosTest.h
@@ -0,0 +1,22 @@
+#import <Foundation/Foundation.h>
+
+@interface E2EIosTest : NSObject
+
+- (BOOL)testE2E:(NSString **)testResult;
+
+@end
+
+#define E2E_IOS_RUNNER(__test_class)                    \
+  @interface __test_class : XCTestCase                  \
+  @end                                                  \
+                                                        \
+  @implementation __test_class                          \
+                                                        \
+  -(void)testE2E {                                      \
+    NSString *testResult;                               \
+    E2EIosTest *e2eIosTest = [[E2EIosTest alloc] init]; \
+    BOOL testPass = [e2eIosTest testE2E:&testResult];   \
+    XCTAssertTrue(testPass, @"%@", testResult);         \
+  }                                                     \
+                                                        \
+  @end
diff --git a/packages/e2e/ios/Classes/E2EIosTest.m b/packages/e2e/ios/Classes/E2EIosTest.m
new file mode 100644
index 0000000..587e3cd
--- /dev/null
+++ b/packages/e2e/ios/Classes/E2EIosTest.m
@@ -0,0 +1,35 @@
+#import "E2EIosTest.h"
+#import "E2EPlugin.h"
+
+@implementation E2EIosTest
+
+- (BOOL)testE2E:(NSString **)testResult {
+  E2EPlugin *e2ePlugin = [E2EPlugin instance];
+  while (!e2ePlugin.testResults) {
+    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.f, NO);
+  }
+  NSDictionary<NSString *, NSString *> *testResults = e2ePlugin.testResults;
+  NSMutableArray<NSString *> *passedTests = [NSMutableArray array];
+  NSMutableArray<NSString *> *failedTests = [NSMutableArray array];
+  NSLog(@"==================== Test Results =====================");
+  for (NSString *test in testResults.allKeys) {
+    NSString *result = testResults[test];
+    if ([result isEqualToString:@"success"]) {
+      NSLog(@"%@ passed.", test);
+      [passedTests addObject:test];
+    } else {
+      NSLog(@"%@ failed.", test);
+      [failedTests addObject:test];
+    }
+  }
+  NSLog(@"================== Test Results End ====================");
+  BOOL testPass = failedTests.count == 0;
+  if (!testPass && testResult) {
+    *testResult =
+        [NSString stringWithFormat:@"Detected failed E2E test(s) %@ among %@",
+                                   failedTests.description, testResults.allKeys.description];
+  }
+  return testPass;
+}
+
+@end
diff --git a/packages/e2e/ios/Classes/E2EPlugin.h b/packages/e2e/ios/Classes/E2EPlugin.h
index 1411dce..b0b296c 100644
--- a/packages/e2e/ios/Classes/E2EPlugin.h
+++ b/packages/e2e/ios/Classes/E2EPlugin.h
@@ -1,4 +1,21 @@
 #import <Flutter/Flutter.h>
 
+NS_ASSUME_NONNULL_BEGIN
+
+/** A Flutter plugin that's responsible for communicating the test results back to iOS XCTest. */
 @interface E2EPlugin : NSObject <FlutterPlugin>
+
+/**
+ * Test results that are sent from Dart when E2E test completes. Before the completion, it is
+ * @c nil.
+ */
+@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *testResults;
+
+/** Fetches the singleton instance of the plugin. */
++ (E2EPlugin *)instance;
+
+- (instancetype)init NS_UNAVAILABLE;
+
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/e2e/ios/Classes/E2EPlugin.m b/packages/e2e/ios/Classes/E2EPlugin.m
index 4f19f3a..aafdf33 100644
--- a/packages/e2e/ios/Classes/E2EPlugin.m
+++ b/packages/e2e/ios/Classes/E2EPlugin.m
@@ -1,16 +1,40 @@
 #import "E2EPlugin.h"
 
-@implementation E2EPlugin
-+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
-  FlutterMethodChannel* channel =
-      [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.dev/e2e"
-                                  binaryMessenger:[registrar messenger]];
-  E2EPlugin* instance = [[E2EPlugin alloc] init];
-  [registrar addMethodCallDelegate:instance channel:channel];
+static NSString *const kE2EPluginChannel = @"plugins.flutter.io/e2e";
+static NSString *const kMethodTestFinished = @"allTestsFinished";
+
+@interface E2EPlugin ()
+
+@property(nonatomic, readwrite) NSDictionary<NSString *, NSString *> *testResults;
+
+@end
+
+@implementation E2EPlugin {
+  NSDictionary<NSString *, NSString *> *_testResults;
 }
 
-- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
-  if ([@"allTestsFinished" isEqualToString:call.method]) {
++ (E2EPlugin *)instance {
+  static dispatch_once_t onceToken;
+  static E2EPlugin *sInstance;
+  dispatch_once(&onceToken, ^{
+    sInstance = [[E2EPlugin alloc] initForRegistration];
+  });
+  return sInstance;
+}
+
+- (instancetype)initForRegistration {
+  return [super init];
+}
+
++ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
+  FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:kE2EPluginChannel
+                                                              binaryMessenger:registrar.messenger];
+  [registrar addMethodCallDelegate:[E2EPlugin instance] channel:channel];
+}
+
+- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
+  if ([kMethodTestFinished isEqual:call.method]) {
+    self.testResults = call.arguments[@"results"];
     result(nil);
   } else {
     result(FlutterMethodNotImplemented);
diff --git a/packages/e2e/pubspec.yaml b/packages/e2e/pubspec.yaml
index 1806c41..504edd4 100644
--- a/packages/e2e/pubspec.yaml
+++ b/packages/e2e/pubspec.yaml
@@ -1,6 +1,6 @@
 name: e2e
 description: Runs tests that use the flutter_test API as integration tests.
-version: 0.2.2+3
+version: 0.2.3
 homepage: https://github.com/flutter/plugins/tree/master/packages/e2e
 
 environment: