Add native iOS screenshots to integration_test (#84611)

diff --git a/dev/integration_tests/ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dev/integration_tests/ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/dev/integration_tests/ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md
index b25c357..0837112 100644
--- a/packages/integration_test/README.md
+++ b/packages/integration_test/README.md
@@ -100,9 +100,9 @@
 You can use `integration_test` to take screenshots of the UI rendered on the mobile device or
 Web browser at a specific time during the test.
 
-This feature is currently supported on Android, and Web.
+This feature is currently supported on Android, iOS, and Web.
 
-#### Android
+#### Android and iOS
 
 **integration_test/screenshot_test.dart**
 
@@ -115,7 +115,7 @@
     // Build the app.
     app.main();
 
-    // This is required prior to taking the screenshot.
+    // This is required prior to taking the screenshot (Android only).
     await binding.convertFlutterSurfaceToImage();
 
     // Trigger a frame.
@@ -126,7 +126,8 @@
 ```
 
 You can use a driver script to pull in the screenshot from the device.
-This way, you can store the images locally on your computer.
+This way, you can store the images locally on your computer.  On iOS, the
+screenshot will also be available in Xcode test results.
 
 **test_driver/integration_test.dart**
 
diff --git a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj
index ee47ce4..a519162 100644
--- a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj
@@ -475,21 +475,11 @@
 			baseConfigurationReference = 09505407E99803EF7AA92DE7 /* Pods-RunnerTests.debug.xcconfig */;
 			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;
@@ -499,20 +489,11 @@
 			baseConfigurationReference = FCE3953801588FC13ED9E898 /* Pods-RunnerTests.release.xcconfig */;
 			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;
@@ -522,20 +503,11 @@
 			baseConfigurationReference = 750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */;
 			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;
diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h b/packages/integration_test/ios/Classes/IntegrationTestIosTest.h
index 5a127da..333b0ec 100644
--- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h
+++ b/packages/integration_test/ios/Classes/IntegrationTestIosTest.h
@@ -4,23 +4,45 @@
 
 #import <Foundation/Foundation.h>
 
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol FLTIntegrationTestScreenshotDelegate;
+
 @interface IntegrationTestIosTest : NSObject
 
-- (BOOL)testIntegrationTest:(NSString **)testResult;
+- (instancetype)initWithScreenshotDelegate:(nullable id<FLTIntegrationTestScreenshotDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Initate dart tests and wait for results.  @c testResult will be set to a string describing the results.
+ *
+ * @return @c YES if all tests succeeded.
+ */
+- (BOOL)testIntegrationTest:(NSString *_Nullable *_Nullable)testResult;
 
 @end
 
 #define INTEGRATION_TEST_IOS_RUNNER(__test_class)                                           \
-  @interface __test_class : XCTestCase                                                      \
+  @interface __test_class : XCTestCase<FLTIntegrationTestScreenshotDelegate>                \
   @end                                                                                      \
                                                                                             \
   @implementation __test_class                                                              \
                                                                                             \
-  -(void)testIntegrationTest {                                                              \
+  - (void)testIntegrationTest {                                                             \
     NSString *testResult;                                                                   \
-    IntegrationTestIosTest *integrationTestIosTest = [[IntegrationTestIosTest alloc] init]; \
+    IntegrationTestIosTest *integrationTestIosTest = integrationTestIosTest = [[IntegrationTestIosTest alloc] initWithScreenshotDelegate:self]; \
     BOOL testPass = [integrationTestIosTest testIntegrationTest:&testResult];               \
     XCTAssertTrue(testPass, @"%@", testResult);                                             \
   }                                                                                         \
                                                                                             \
+  - (void)didTakeScreenshot:(UIImage *)screenshot attachmentName:(NSString *)name {         \
+    XCTAttachment *attachment = [XCTAttachment attachmentWithImage:screenshot];             \
+    attachment.lifetime = XCTAttachmentLifetimeKeepAlways;                                  \
+    if (name != nil) {                                                                      \
+      attachment.name = name;                                                               \
+    }                                                                                       \
+    [self addAttachment:attachment];                                                        \
+  }                                                                                         \
+                                                                                            \
   @end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m b/packages/integration_test/ios/Classes/IntegrationTestIosTest.m
index c989f8e..6a54ed2 100644
--- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m
+++ b/packages/integration_test/ios/Classes/IntegrationTestIosTest.m
@@ -5,10 +5,26 @@
 #import "IntegrationTestIosTest.h"
 #import "IntegrationTestPlugin.h"
 
+@interface IntegrationTestIosTest()
+@property (nonatomic) IntegrationTestPlugin *integrationTestPlugin;
+@end
+
 @implementation IntegrationTestIosTest
 
+- (instancetype)initWithScreenshotDelegate:(id<FLTIntegrationTestScreenshotDelegate>)delegate {
+  self = [super init];
+  _integrationTestPlugin = [IntegrationTestPlugin instance];
+  _integrationTestPlugin.screenshotDelegate = delegate;
+  return self;
+}
+
+- (instancetype)init {
+  return [self initWithScreenshotDelegate:nil];
+}
+
 - (BOOL)testIntegrationTest:(NSString **)testResult {
-  IntegrationTestPlugin *integrationTestPlugin = [IntegrationTestPlugin instance];
+  IntegrationTestPlugin *integrationTestPlugin = self.integrationTestPlugin;
+
   UIViewController *rootViewController =
       [[[[UIApplication sharedApplication] delegate] window] rootViewController];
   if (![rootViewController isKindOfClass:[FlutterViewController class]]) {
diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h b/packages/integration_test/ios/Classes/IntegrationTestPlugin.h
index d73246a..9684835 100644
--- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h
+++ b/packages/integration_test/ios/Classes/IntegrationTestPlugin.h
@@ -6,14 +6,20 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+@protocol FLTIntegrationTestScreenshotDelegate
+
+/** This will be called when a dart integration test triggers a window screenshot with  @c takeScreenshot. */
+- (void)didTakeScreenshot:(UIImage *)screenshot attachmentName:(nullable NSString *)name;
+
+@end
+
 /** A Flutter plugin that's responsible for communicating the test results back
  * to iOS XCTest. */
 @interface IntegrationTestPlugin : NSObject <FlutterPlugin>
 
 /**
  * Test results that are sent from Dart when integration test completes. Before the
- * completion, it is
- * @c nil.
+ * completion, it is @c nil.
  */
 @property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *testResults;
 
@@ -24,6 +30,8 @@
 
 - (instancetype)init NS_UNAVAILABLE;
 
+@property(weak, nonatomic) id<FLTIntegrationTestScreenshotDelegate> screenshotDelegate;
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m
index 8d8f8ae..82d2635 100644
--- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m
+++ b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m
@@ -2,10 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+@import UIKit;
+
 #import "IntegrationTestPlugin.h"
 
 static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test";
 static NSString *const kMethodTestFinished = @"allTestsFinished";
+static NSString *const kMethodScreenshot = @"captureScreenshot";
+static NSString *const kMethodConvertSurfaceToImage = @"convertFlutterSurfaceToImage";
+static NSString *const kMethodRevertImage = @"revertFlutterImage";
 
 @interface IntegrationTestPlugin ()
 
@@ -39,20 +44,55 @@
 
 - (void)setupChannels:(id<FlutterBinaryMessenger>)binaryMessenger {
   FlutterMethodChannel *channel =
-      [FlutterMethodChannel methodChannelWithName:kIntegrationTestPluginChannel
-                                  binaryMessenger:binaryMessenger];
+  [FlutterMethodChannel methodChannelWithName:kIntegrationTestPluginChannel
+                              binaryMessenger:binaryMessenger];
   [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
     [self handleMethodCall:call result:result];
   }];
 }
 
 - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
-  if ([kMethodTestFinished isEqual:call.method]) {
+  if ([call.method isEqualToString:kMethodTestFinished]) {
     self.testResults = call.arguments[@"results"];
     result(nil);
+  } else if ([call.method isEqualToString:kMethodScreenshot]) {
+    // If running as a native Xcode test, attach to test.
+    UIImage *screenshot = [self capturePngScreenshot];
+    NSString *name = call.arguments[@"name"];
+    [self.screenshotDelegate didTakeScreenshot:screenshot attachmentName:name];
+
+    // Also pass back along the channel for the driver to handle.
+    NSData *pngData = UIImagePNGRepresentation(screenshot);
+    result([FlutterStandardTypedData typedDataWithBytes:pngData]);
+  } else if ([call.method isEqualToString:kMethodConvertSurfaceToImage]
+             || [call.method isEqualToString:kMethodRevertImage]) {
+    // Android only, no-op on iOS.
+    result(nil);
   } else {
     result(FlutterMethodNotImplemented);
   }
 }
 
+- (UIImage *)capturePngScreenshot {
+  UIWindow *window = [UIApplication.sharedApplication.windows
+                      filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"keyWindow = YES"]].firstObject;
+  CGRect screenshotBounds = window.bounds;
+  UIImage *image;
+
+  if (@available(iOS 10, *)) {
+    UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithBounds:screenshotBounds];
+
+    image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
+      [window drawViewHierarchyInRect:screenshotBounds afterScreenUpdates:YES];
+    }];
+  } else {
+    UIGraphicsBeginImageContextWithOptions(screenshotBounds.size, NO, UIScreen.mainScreen.scale);
+    [window drawViewHierarchyInRect:screenshotBounds afterScreenUpdates:YES];
+    image = UIGraphicsGetImageFromCurrentImageContext();
+    UIGraphicsEndImageContext();
+  }
+
+  return image;
+}
+
 @end
diff --git a/packages/integration_test/ios/integration_test.podspec b/packages/integration_test/ios/integration_test.podspec
index 4f92e65..fb24cb0 100644
--- a/packages/integration_test/ios/integration_test.podspec
+++ b/packages/integration_test/ios/integration_test.podspec
@@ -18,6 +18,8 @@
   s.source_files = 'Classes/**/*'
   s.public_header_files = 'Classes/**/*.h'
   s.dependency 'Flutter'
+  s.ios.framework  = 'UIKit'
+
   s.platform = :ios, '8.0'
   s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
 end
diff --git a/packages/integration_test/lib/_callback_io.dart b/packages/integration_test/lib/_callback_io.dart
index 8717305..4639f13 100644
--- a/packages/integration_test/lib/_callback_io.dart
+++ b/packages/integration_test/lib/_callback_io.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io' show Platform;
 import 'dart:ui';
 
 import 'package:flutter/services.dart';
@@ -60,37 +61,41 @@
     // comes up in the future. For example: `WebCallbackManager.cleanup`.
   }
 
-  // Whether the Flutter surface uses an Image.
-  bool _usesFlutterImage = false;
+  // [convertFlutterSurfaceToImage] has been called and [takeScreenshot] is ready to capture the surface (Android only).
+  bool _isSurfaceRendered = false;
 
   @override
   Future<void> convertFlutterSurfaceToImage() async {
-    assert(!_usesFlutterImage, 'Surface already converted to an image');
+    if (!Platform.isAndroid) {
+      // No-op on other platforms.
+      return;
+    }
+    assert(!_isSurfaceRendered, 'Surface already converted to an image');
     await integrationTestChannel.invokeMethod<void>(
       'convertFlutterSurfaceToImage',
       null,
     );
-    _usesFlutterImage = true;
+    _isSurfaceRendered = true;
 
     addTearDown(() async {
-      assert(_usesFlutterImage, 'Surface is not an image');
+      assert(_isSurfaceRendered, 'Surface is not an image');
       await integrationTestChannel.invokeMethod<void>(
         'revertFlutterImage',
         null,
       );
-      _usesFlutterImage = false;
+      _isSurfaceRendered = false;
     });
   }
 
   @override
   Future<Map<String, dynamic>> takeScreenshot(String screenshot) async {
-    if (!_usesFlutterImage) {
+    if (Platform.isAndroid && !_isSurfaceRendered) {
       throw StateError('Call convertFlutterSurfaceToImage() before taking a screenshot');
     }
     integrationTestChannel.setMethodCallHandler(_onMethodChannelCall);
     final List<int>? rawBytes = await integrationTestChannel.invokeMethod<List<int>>(
       'captureScreenshot',
-      null,
+      <String, dynamic>{'name': screenshot},
     );
     if (rawBytes == null) {
       throw StateError('Expected a list of bytes, but instead captureScreenshot returned null');