[camera]writing file on background queue (#4721)

diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 9b32af2..f846774 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.4+10
+
+* iOS performance improvement by moving file writing from the main queue to a background IO queue. 
+
 ## 0.9.4+9
 
 * iOS performance improvement by moving sample buffer handling from the main queue to a background session queue. 
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
index 65a6bbd..ac39de2 100644
--- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 46;
+	objectVersion = 50;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -22,6 +22,7 @@
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
 		E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; };
 		E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; };
+		E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; };
 		E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; };
 		E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; };
 		E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; };
@@ -83,6 +84,7 @@
 		A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
 		E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = "<group>"; };
 		E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = "<group>"; };
+		E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = "<group>"; };
 		E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = "<group>"; };
 		E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = "<group>"; };
 		E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = "<group>"; };
@@ -125,6 +127,7 @@
 				E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */,
 				E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */,
 				E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */,
+				E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */,
 				E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */,
 				E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
 				F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */,
@@ -399,6 +402,7 @@
 				E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */,
 				03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */,
 				E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
+				E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */,
 				F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */,
 				334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
 				E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */,
diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m
new file mode 100644
index 0000000..b6ea84d
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m
@@ -0,0 +1,133 @@
+// 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.
+
+@import camera;
+@import camera.Test;
+@import AVFoundation;
+@import XCTest;
+#import <OCMock/OCMock.h>
+
+@interface FLTSavePhotoDelegateTests : XCTestCase
+
+@end
+
+@implementation FLTSavePhotoDelegateTests
+
+- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture {
+  NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil];
+  dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
+  id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
+  FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
+                                                                       result:mockResult
+                                                                      ioQueue:ioQueue];
+
+  [delegate handlePhotoCaptureResultWithError:error
+                            photoDataProvider:^NSData * {
+                              return nil;
+                            }];
+  OCMVerify([mockResult sendError:error]);
+}
+
+- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite {
+  XCTestExpectation *resultExpectation =
+      [self expectationWithDescription:@"Must send IOError to the result if failed to write file."];
+  dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
+  id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
+
+  NSError *ioError = [NSError errorWithDomain:@"IOError"
+                                         code:0
+                                     userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}];
+
+  OCMStub([mockResult sendErrorWithCode:@"IOError"
+                                message:@"Unable to write file"
+                                details:ioError.localizedDescription])
+      .andDo(^(NSInvocation *invocation) {
+        [resultExpectation fulfill];
+      });
+  FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
+                                                                       result:mockResult
+                                                                      ioQueue:ioQueue];
+
+  // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
+  // `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
+  id mockData = OCMPartialMock([NSData data]);
+  OCMStub([mockData writeToFile:OCMOCK_ANY
+                        options:NSDataWritingAtomic
+                          error:[OCMArg setTo:ioError]])
+      .andReturn(NO);
+  [delegate handlePhotoCaptureResultWithError:nil
+                            photoDataProvider:^NSData * {
+                              return mockData;
+                            }];
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite {
+  XCTestExpectation *resultExpectation = [self
+      expectationWithDescription:@"Must send file path to the result if success to write file."];
+
+  dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
+  id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
+  FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
+                                                                       result:mockResult
+                                                                      ioQueue:ioQueue];
+  OCMStub([mockResult sendSuccessWithData:delegate.path]).andDo(^(NSInvocation *invocation) {
+    [resultExpectation fulfill];
+  });
+
+  // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
+  // `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
+  id mockData = OCMPartialMock([NSData data]);
+  OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]])
+      .andReturn(YES);
+
+  [delegate handlePhotoCaptureResultWithError:nil
+                            photoDataProvider:^NSData * {
+                              return mockData;
+                            }];
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue {
+  XCTestExpectation *dataProviderQueueExpectation =
+      [self expectationWithDescription:@"Data provider must run on io queue."];
+  XCTestExpectation *writeFileQueueExpectation =
+      [self expectationWithDescription:@"File writing must run on io queue"];
+  XCTestExpectation *resultExpectation = [self
+      expectationWithDescription:@"Must send file path to the result if success to write file."];
+
+  dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL);
+  const char *ioQueueSpecific = "io_queue_specific";
+  dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL);
+  id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]);
+  OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
+    [resultExpectation fulfill];
+  });
+
+  // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g.
+  // `XCTRunnerIDESession::logDebugMessage:`) on a private queue.
+  id mockData = OCMPartialMock([NSData data]);
+  OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]])
+      .andDo(^(NSInvocation *invocation) {
+        if (dispatch_get_specific(ioQueueSpecific)) {
+          [writeFileQueueExpectation fulfill];
+        }
+      })
+      .andReturn(YES);
+
+  FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test"
+                                                                       result:mockResult
+                                                                      ioQueue:ioQueue];
+  [delegate handlePhotoCaptureResultWithError:nil
+                            photoDataProvider:^NSData * {
+                              if (dispatch_get_specific(ioQueueSpecific)) {
+                                [dataProviderQueueExpectation fulfill];
+                              }
+                              return mockData;
+                            }];
+
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
index a695728..529c658 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
@@ -9,5 +9,11 @@
     header "CameraProperties.h"
     header "FLTCam.h"
     header "FLTCam_Test.h"
+    header "FLTSavePhotoDelegate_Test.h"
+    header "FLTThreadSafeEventChannel.h"
+    header "FLTThreadSafeFlutterResult.h"
+    header "FLTThreadSafeMethodChannel.h"
+    header "FLTThreadSafeTextureRegistry.h"
+    header "QueueHelper.h"
   }
 }
diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m
index 82ac671..94f9850 100644
--- a/packages/camera/camera/ios/Classes/FLTCam.m
+++ b/packages/camera/camera/ios/Classes/FLTCam.m
@@ -4,6 +4,7 @@
 
 #import "FLTCam.h"
 #import "FLTCam_Test.h"
+#import "FLTSavePhotoDelegate.h"
 
 @import CoreMotion;
 #import <libkern/OSAtomic.h>
@@ -41,71 +42,6 @@
 }
 @end
 
-@interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate>
-@property(readonly, nonatomic) NSString *path;
-@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result;
-@end
-
-@implementation FLTSavePhotoDelegate {
-  /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer.
-  FLTSavePhotoDelegate *selfReference;
-}
-
-- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result {
-  self = [super init];
-  NSAssert(self, @"super init cannot be nil");
-  _path = path;
-  selfReference = self;
-  _result = result;
-  return self;
-}
-
-- (void)captureOutput:(AVCapturePhotoOutput *)output
-    didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer
-                previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer
-                        resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
-                         bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings
-                                   error:(NSError *)error API_AVAILABLE(ios(10)) {
-  selfReference = nil;
-  if (error) {
-    [_result sendError:error];
-    return;
-  }
-
-  NSData *data = [AVCapturePhotoOutput
-      JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer
-                            previewPhotoSampleBuffer:previewPhotoSampleBuffer];
-
-  // TODO(sigurdm): Consider writing file asynchronously.
-  bool success = [data writeToFile:_path atomically:YES];
-
-  if (!success) {
-    [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil];
-    return;
-  }
-  [_result sendSuccessWithData:_path];
-}
-
-- (void)captureOutput:(AVCapturePhotoOutput *)output
-    didFinishProcessingPhoto:(AVCapturePhoto *)photo
-                       error:(NSError *)error API_AVAILABLE(ios(11.0)) {
-  selfReference = nil;
-  if (error) {
-    [_result sendError:error];
-    return;
-  }
-
-  NSData *photoData = [photo fileDataRepresentation];
-
-  bool success = [photoData writeToFile:_path atomically:YES];
-  if (!success) {
-    [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil];
-    return;
-  }
-  [_result sendSuccessWithData:_path];
-}
-@end
-
 @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
                       AVCaptureAudioDataOutputSampleBufferDelegate>
 
@@ -138,8 +74,11 @@
 @property(assign, nonatomic) CMTime audioTimeOffset;
 @property(nonatomic) CMMotionManager *motionManager;
 @property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor;
-// All FLTCam's state access and capture session related operations should be on run on this queue.
+/// All FLTCam's state access and capture session related operations should be on run on this queue.
 @property(strong, nonatomic) dispatch_queue_t captureSessionQueue;
+/// The queue on which captured photos (not videos) are wrote to disk.
+/// Videos are wrote to disk by `videoAdaptor` on an internal queue managed by AVFoundation.
+@property(strong, nonatomic) dispatch_queue_t photoIOQueue;
 @property(assign, nonatomic) UIDeviceOrientation deviceOrientation;
 @end
 
@@ -162,6 +101,7 @@
   }
   _enableAudio = enableAudio;
   _captureSessionQueue = captureSessionQueue;
+  _photoIOQueue = dispatch_queue_create("io.flutter.camera.photoIOQueue", NULL);
   _captureSession = [[AVCaptureSession alloc] init];
   _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName];
   _flashMode = _captureDevice.hasFlash ? FLTFlashModeAuto : FLTFlashModeOff;
@@ -280,9 +220,11 @@
     return;
   }
 
-  [_capturePhotoOutput capturePhotoWithSettings:settings
-                                       delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
-                                                                                    result:result]];
+  [_capturePhotoOutput
+      capturePhotoWithSettings:settings
+                      delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
+                                                                   result:result
+                                                                  ioQueue:self.photoIOQueue]];
 }
 
 - (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h
new file mode 100644
index 0000000..a773b46
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h
@@ -0,0 +1,37 @@
+// 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.
+
+@import AVFoundation;
+@import Foundation;
+@import Flutter;
+
+#import "FLTThreadSafeFlutterResult.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ Delegate object that handles photo capture results.
+ */
+@interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate>
+/// The file path for the captured photo.
+@property(readonly, nonatomic) NSString *path;
+/// The thread safe flutter result wrapper to report the result.
+@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result;
+/// The queue on which captured photos are wrote to disk.
+@property(strong, nonatomic) dispatch_queue_t ioQueue;
+/// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer.
+@property(strong, nonatomic, nullable) FLTSavePhotoDelegate *selfReference;
+
+/**
+ * Initialize a photo capture delegate.
+ * @param path the path for captured photo file.
+ * @param result the thread safe flutter result wrapper to report the result.
+ * @param ioQueue the queue on which captured photos are wrote to disk.
+ */
+- (instancetype)initWithPath:(NSString *)path
+                      result:(FLTThreadSafeFlutterResult *)result
+                     ioQueue:(dispatch_queue_t)ioQueue;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m
new file mode 100644
index 0000000..8dadfec
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m
@@ -0,0 +1,65 @@
+// 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.
+
+#import "FLTSavePhotoDelegate.h"
+
+@implementation FLTSavePhotoDelegate
+
+- (instancetype)initWithPath:(NSString *)path
+                      result:(FLTThreadSafeFlutterResult *)result
+                     ioQueue:(dispatch_queue_t)ioQueue {
+  self = [super init];
+  NSAssert(self, @"super init cannot be nil");
+  _path = path;
+  _selfReference = self;
+  _result = result;
+  _ioQueue = ioQueue;
+  return self;
+}
+
+- (void)handlePhotoCaptureResultWithError:(NSError *)error
+                        photoDataProvider:(NSData * (^)(void))photoDataProvider {
+  self.selfReference = nil;
+  if (error) {
+    [self.result sendError:error];
+    return;
+  }
+  dispatch_async(self.ioQueue, ^{
+    NSData *data = photoDataProvider();
+    NSError *ioError;
+    if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) {
+      [self.result sendSuccessWithData:self.path];
+    } else {
+      [self.result sendErrorWithCode:@"IOError"
+                             message:@"Unable to write file"
+                             details:ioError.localizedDescription];
+    }
+  });
+}
+
+- (void)captureOutput:(AVCapturePhotoOutput *)output
+    didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer
+                previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer
+                        resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
+                         bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings
+                                   error:(NSError *)error API_AVAILABLE(ios(10)) {
+  [self handlePhotoCaptureResultWithError:error
+                        photoDataProvider:^NSData * {
+                          return [AVCapturePhotoOutput
+                              JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer
+                                                    previewPhotoSampleBuffer:
+                                                        previewPhotoSampleBuffer];
+                        }];
+}
+
+- (void)captureOutput:(AVCapturePhotoOutput *)output
+    didFinishProcessingPhoto:(AVCapturePhoto *)photo
+                       error:(NSError *)error API_AVAILABLE(ios(11.0)) {
+  [self handlePhotoCaptureResultWithError:error
+                        photoDataProvider:^NSData * {
+                          return [photo fileDataRepresentation];
+                        }];
+}
+
+@end
diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h
new file mode 100644
index 0000000..c0b77c7
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h
@@ -0,0 +1,17 @@
+// 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.
+
+#import "FLTSavePhotoDelegate.h"
+
+/**
+ API exposed for unit tests.
+ */
+@interface FLTSavePhotoDelegate ()
+
+/// Handler to write captured photo data into a file.
+/// @param error the capture error.
+/// @param photoDataProvider a closure that provides photo data.
+- (void)handlePhotoCaptureResultWithError:(NSError *)error
+                        photoDataProvider:(NSData * (^)(void))photoDataProvider;
+@end
diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h
index 428b125..5c39401 100644
--- a/packages/camera/camera/ios/Classes/camera-umbrella.h
+++ b/packages/camera/camera/ios/Classes/camera-umbrella.h
@@ -4,11 +4,6 @@
 
 #import <Foundation/Foundation.h>
 #import <camera/CameraPlugin.h>
-#import <camera/FLTThreadSafeEventChannel.h>
-#import <camera/FLTThreadSafeFlutterResult.h>
-#import <camera/FLTThreadSafeMethodChannel.h>
-#import <camera/FLTThreadSafeTextureRegistry.h>
-#import <camera/QueueHelper.h>
 
 FOUNDATION_EXPORT double cameraVersionNumber;
 FOUNDATION_EXPORT const unsigned char cameraVersionString[];
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 0f1a0f0..4516e55 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
   Dart.
 repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.4+9
+version: 0.9.4+10
 
 environment:
   sdk: ">=2.14.0 <3.0.0"