[image_picker] Migrate iOS to Pigeon and improve state handling (#5285)

diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md
index 3472ade..3380d14 100644
--- a/packages/image_picker/image_picker_ios/CHANGELOG.md
+++ b/packages/image_picker/image_picker_ios/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.8.5
+
+* Switches to an in-package method channel based on Pigeon.
+* Fixes invalid casts when selecting multiple images on versions of iOS before
+  14.0.
+
 ## 0.8.4+11
 
 * Splits from `image_picker` as a federated implementation.
diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m
index 8df5299..04d4911 100644
--- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m
+++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m
@@ -47,14 +47,15 @@
 
   // Run test
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
-                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
   UIImagePickerController *controller = [[UIImagePickerController alloc] init];
   [plugin setImagePickerControllerOverrides:@[ controller ]];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
+
+  [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
+                                                            camera:FLTSourceCameraRear]
+                      maxSize:[[FLTMaxSize alloc] init]
+                      quality:nil
+                   completion:^(NSString *_Nullable result, FlutterError *_Nullable error){
+                   }];
 
   XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear);
 }
@@ -78,14 +79,15 @@
 
   // Run test
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
-                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}];
   UIImagePickerController *controller = [[UIImagePickerController alloc] init];
   [plugin setImagePickerControllerOverrides:@[ controller ]];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
+
+  [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
+                                                            camera:FLTSourceCameraFront]
+                      maxSize:[[FLTMaxSize alloc] init]
+                      quality:nil
+                   completion:^(NSString *_Nullable result, FlutterError *_Nullable error){
+                   }];
 
   XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront);
 }
@@ -109,14 +111,14 @@
 
   // Run test
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"pickVideo"
-                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
   UIImagePickerController *controller = [[UIImagePickerController alloc] init];
   [plugin setImagePickerControllerOverrides:@[ controller ]];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
+
+  [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
+                                                            camera:FLTSourceCameraRear]
+                  maxDuration:nil
+                   completion:^(NSString *_Nullable result, FlutterError *_Nullable error){
+                   }];
 
   XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear);
 }
@@ -141,14 +143,14 @@
 
   // Run test
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"pickVideo"
-                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}];
   UIImagePickerController *controller = [[UIImagePickerController alloc] init];
   [plugin setImagePickerControllerOverrides:@[ controller ]];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
+
+  [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
+                                                            camera:FLTSourceCameraFront]
+                  maxDuration:nil
+                   completion:^(NSString *_Nullable result, FlutterError *_Nullable error){
+                   }];
 
   XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront);
 }
@@ -165,17 +167,12 @@
 
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
   [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]];
-  FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickMultiImage"
-                                                              arguments:@{
-                                                                @"maxWidth" : @(100),
-                                                                @"maxHeight" : @(200),
-                                                                @"imageQuality" : @(50),
-                                                              }];
 
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
-
+  [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)]
+                            quality:@(50)
+                         completion:^(NSArray<NSString *> *_Nullable result,
+                                      FlutterError *_Nullable error){
+                         }];
   OCMVerify(times(1),
             [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]);
 }
@@ -187,17 +184,15 @@
     return;
   }
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
-                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}];
   UIImagePickerController *controller = [[UIImagePickerController alloc] init];
   plugin.imagePickerControllerOverrides = @[ controller ];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
-  plugin.result = ^(id result) {
 
-  };
+  [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
+                                                            camera:FLTSourceCameraRear]
+                      maxSize:[[FLTMaxSize alloc] init]
+                      quality:nil
+                   completion:^(NSString *_Nullable result, FlutterError *_Nullable error){
+                   }];
 
   // To ensure the flow does not crash by multiple cancel call
   [plugin imagePickerControllerDidCancel:controller];
@@ -208,14 +203,15 @@
 
 - (void)testPickingVideoWithDuration {
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call = [FlutterMethodCall
-      methodCallWithMethodName:@"pickVideo"
-                     arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}];
   UIImagePickerController *controller = [[UIImagePickerController alloc] init];
   [plugin setImagePickerControllerOverrides:@[ controller ]];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
+
+  [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
+                                                            camera:FLTSourceCameraRear]
+                  maxDuration:@(95)
+                   completion:^(NSString *_Nullable result, FlutterError *_Nullable error){
+                   }];
+
   XCTAssertEqual(controller.videoMaximumDuration, 95);
 }
 
@@ -231,37 +227,17 @@
   XCTAssertEqual([plugin viewControllerWithWindow:window], vc2);
 }
 
-- (void)testPluginMultiImagePathIsNil {
-  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-
-  dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
-  __block FlutterError *pickImageResult = nil;
-
-  plugin.result = ^(id _Nullable r) {
-    pickImageResult = r;
-    dispatch_semaphore_signal(resultSemaphore);
-  };
-  [plugin handleSavedPathList:nil];
-
-  dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
-
-  XCTAssertEqualObjects(pickImageResult.code, @"create_error");
-}
-
 - (void)testPluginMultiImagePathHasNullItem {
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  NSMutableArray *pathList = [NSMutableArray new];
-
-  [pathList addObject:[NSNull null]];
 
   dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
   __block FlutterError *pickImageResult = nil;
-
-  plugin.result = ^(id _Nullable r) {
-    pickImageResult = r;
-    dispatch_semaphore_signal(resultSemaphore);
-  };
-  [plugin handleSavedPathList:pathList];
+  plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
+        pickImageResult = error;
+        dispatch_semaphore_signal(resultSemaphore);
+      }];
+  [plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]];
 
   dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
 
@@ -270,19 +246,17 @@
 
 - (void)testPluginMultiImagePathHasItem {
   FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  NSString *savedPath = @"test";
-  NSMutableArray *pathList = [NSMutableArray new];
-
-  [pathList addObject:savedPath];
+  NSArray *pathList = @[ @"test" ];
 
   dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
   __block id pickImageResult = nil;
 
-  plugin.result = ^(id _Nullable r) {
-    pickImageResult = r;
-    dispatch_semaphore_signal(resultSemaphore);
-  };
-  [plugin handleSavedPathList:pathList];
+  plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
+        pickImageResult = result;
+        dispatch_semaphore_signal(resultSemaphore);
+      }];
+  [plugin sendCallResultWithSavedPathList:pathList];
 
   dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
 
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
index cc841d6..76ed962 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
@@ -16,15 +16,18 @@
 #import "FLTImagePickerMetaDataUtil.h"
 #import "FLTImagePickerPhotoAssetUtil.h"
 #import "FLTPHPickerSaveImageToPathOperation.h"
+#import "messages.g.h"
 
-/**
- * Returns the value for the given key in 'dict', or nil if the value is
- * NSNull.
- */
-id GetNullableValueForKey(NSDictionary *dict, NSString *key) {
-  id value = dict[key];
-  return value == [NSNull null] ? nil : value;
+@implementation FLTImagePickerMethodCallContext
+- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result {
+  if (self = [super init]) {
+    _result = [result copy];
+  }
+  return self;
 }
+@end
+
+#pragma mark -
 
 @interface FLTImagePickerPlugin () <UINavigationControllerDelegate,
                                     UIImagePickerControllerDelegate,
@@ -32,16 +35,6 @@
                                     UIAdaptivePresentationControllerDelegate>
 
 /**
- * The maximum amount of images that are allowed to be picked.
- */
-@property(assign, nonatomic) int maxImagesAllowed;
-
-/**
- * The arguments that are passed in from the Flutter method call.
- */
-@property(copy, nonatomic) NSDictionary *arguments;
-
-/**
  * The PHPickerViewController instance used to pick multiple
  * images.
  */
@@ -58,19 +51,13 @@
 
 @end
 
-static const int SOURCE_CAMERA = 0;
-static const int SOURCE_GALLERY = 1;
-
 typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPickerClassType };
 
 @implementation FLTImagePickerPlugin
 
 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
-  FlutterMethodChannel *channel =
-      [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker"
-                                  binaryMessenger:[registrar messenger]];
   FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new];
-  [registrar addMethodCallDelegate:instance channel:channel];
+  FLTImagePickerApiSetup(registrar.messenger, instance);
 }
 
 - (UIImagePickerController *)createImagePickerController {
@@ -107,130 +94,180 @@
 }
 
 /**
- * Returns the UIImagePickerControllerCameraDevice to use given [arguments].
+ * Returns the UIImagePickerControllerCameraDevice to use given [source].
  *
- * If the cameraDevice value that is fetched from arguments is 1 then returns
- * UIImagePickerControllerCameraDeviceFront. If the cameraDevice value that is fetched
- * from arguments is 0 then returns UIImagePickerControllerCameraDeviceRear.
- *
- * @param arguments that should be used to get cameraDevice value.
+ * @param source The source specification from Dart.
  */
-- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments {
-  NSInteger cameraDevice = [arguments[@"cameraDevice"] intValue];
-  return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront
-                             : UIImagePickerControllerCameraDeviceRear;
+- (UIImagePickerControllerCameraDevice)cameraDeviceForSource:(FLTSourceSpecification *)source {
+  switch (source.camera) {
+    case FLTSourceCameraFront:
+      return UIImagePickerControllerCameraDeviceFront;
+    case FLTSourceCameraRear:
+      return UIImagePickerControllerCameraDeviceRear;
+  }
 }
 
-- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) {
+- (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)context
+    API_AVAILABLE(ios(14)) {
   PHPickerConfiguration *config =
       [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary];
-  config.selectionLimit = maxImagesAllowed;  // Setting to zero allow us to pick unlimited photos
+  config.selectionLimit = context.maxImageCount;
   config.filter = [PHPickerFilter imagesFilter];
 
   _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config];
   _pickerViewController.delegate = self;
   _pickerViewController.presentationController.delegate = self;
-
-  self.maxImagesAllowed = maxImagesAllowed;
+  self.callContext = context;
 
   [self checkPhotoAuthorizationForAccessLevel];
 }
 
-- (void)launchUIImagePickerWithSource:(int)imageSource {
+- (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source
+                              context:(nonnull FLTImagePickerMethodCallContext *)context {
   UIImagePickerController *imagePickerController = [self createImagePickerController];
   imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
   imagePickerController.delegate = self;
   imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
+  self.callContext = context;
 
-  self.maxImagesAllowed = 1;
-
-  switch (imageSource) {
-    case SOURCE_CAMERA:
-      [self checkCameraAuthorizationWithImagePicker:imagePickerController];
+  switch (source.type) {
+    case FLTSourceTypeCamera:
+      [self checkCameraAuthorizationWithImagePicker:imagePickerController
+                                             camera:[self cameraDeviceForSource:source]];
       break;
-    case SOURCE_GALLERY:
+    case FLTSourceTypeGallery:
       [self checkPhotoAuthorizationWithImagePicker:imagePickerController];
       break;
     default:
-      self.result([FlutterError errorWithCode:@"invalid_source"
-                                      message:@"Invalid image source."
-                                      details:nil]);
+      [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source"
+                                                        message:@"Invalid image source."
+                                                        details:nil]];
       break;
   }
 }
 
-- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
-  if (self.result) {
-    self.result([FlutterError errorWithCode:@"multiple_request"
-                                    message:@"Cancelled by a second request"
-                                    details:nil]);
-    self.result = nil;
-  }
+#pragma mark - FLTImagePickerApi
 
-  self.result = result;
-  _arguments = call.arguments;
+- (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source
+                    maxSize:(nonnull FLTMaxSize *)maxSize
+                    quality:(nullable NSNumber *)imageQuality
+                 completion:
+                     (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
+  [self cancelInProgressCall];
+  FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^void(NSArray<NSString *> *paths, FlutterError *error) {
+        if (paths && paths.count != 1) {
+          completion(nil, [FlutterError errorWithCode:@"invalid_result"
+                                              message:@"Incorrect number of return paths provided"
+                                              details:nil]);
+        }
+        completion(paths.firstObject, error);
+      }];
+  context.maxSize = maxSize;
+  context.imageQuality = imageQuality;
+  context.maxImageCount = 1;
 
-  if ([@"pickImage" isEqualToString:call.method]) {
-    int imageSource = [call.arguments[@"source"] intValue];
-
-    if (imageSource == SOURCE_GALLERY) {  // Capture is not possible with PHPicker
-      if (@available(iOS 14, *)) {
-        // PHPicker is used
-        [self pickImageWithPHPicker:1];
-      } else {
-        // UIImagePicker is used
-        [self launchUIImagePickerWithSource:imageSource];
-      }
-    } else {
-      [self launchUIImagePickerWithSource:imageSource];
-    }
-  } else if ([@"pickMultiImage" isEqualToString:call.method]) {
+  if (source.type == FLTSourceTypeGallery) {  // Capture is not possible with PHPicker
     if (@available(iOS 14, *)) {
-      [self pickImageWithPHPicker:0];
+      [self launchPHPickerWithContext:context];
     } else {
-      [self launchUIImagePickerWithSource:SOURCE_GALLERY];
-    }
-  } else if ([@"pickVideo" isEqualToString:call.method]) {
-    UIImagePickerController *imagePickerController = [self createImagePickerController];
-    imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
-    imagePickerController.delegate = self;
-    imagePickerController.mediaTypes = @[
-      (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo,
-      (NSString *)kUTTypeMPEG4
-    ];
-    imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh;
-
-    int imageSource = [call.arguments[@"source"] intValue];
-    if ([call.arguments[@"maxDuration"] isKindOfClass:[NSNumber class]]) {
-      NSTimeInterval max = [call.arguments[@"maxDuration"] doubleValue];
-      imagePickerController.videoMaximumDuration = max;
-    }
-
-    switch (imageSource) {
-      case SOURCE_CAMERA:
-        [self checkCameraAuthorizationWithImagePicker:imagePickerController];
-        break;
-      case SOURCE_GALLERY:
-        [self checkPhotoAuthorizationWithImagePicker:imagePickerController];
-        break;
-      default:
-        result([FlutterError errorWithCode:@"invalid_source"
-                                   message:@"Invalid video source."
-                                   details:nil]);
-        break;
+      [self launchUIImagePickerWithSource:source context:context];
     }
   } else {
-    result(FlutterMethodNotImplemented);
+    [self launchUIImagePickerWithSource:source context:context];
   }
 }
 
-- (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerController {
+- (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize
+                          quality:(nullable NSNumber *)imageQuality
+                       completion:(nonnull void (^)(NSArray<NSString *> *_Nullable,
+                                                    FlutterError *_Nullable))completion {
+  FLTImagePickerMethodCallContext *context =
+      [[FLTImagePickerMethodCallContext alloc] initWithResult:completion];
+  context.maxSize = maxSize;
+  context.imageQuality = imageQuality;
+
+  if (@available(iOS 14, *)) {
+    [self launchPHPickerWithContext:context];
+  } else {
+    // Camera is ignored for gallery mode, so the value here is arbitrary.
+    [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery
+                                                                      camera:FLTSourceCameraRear]
+                                context:context];
+  }
+}
+
+- (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source
+                maxDuration:(nullable NSNumber *)maxDurationSeconds
+                 completion:
+                     (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
+  FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^void(NSArray<NSString *> *paths, FlutterError *error) {
+        if (paths && paths.count != 1) {
+          completion(nil, [FlutterError errorWithCode:@"invalid_result"
+                                              message:@"Incorrect number of return paths provided"
+                                              details:nil]);
+        }
+        completion(paths.firstObject, error);
+      }];
+  context.maxImageCount = 1;
+
+  UIImagePickerController *imagePickerController = [self createImagePickerController];
+  imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
+  imagePickerController.delegate = self;
+  imagePickerController.mediaTypes = @[
+    (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo,
+    (NSString *)kUTTypeMPEG4
+  ];
+  imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh;
+
+  if (maxDurationSeconds) {
+    NSTimeInterval max = [maxDurationSeconds doubleValue];
+    imagePickerController.videoMaximumDuration = max;
+  }
+
+  self.callContext = context;
+
+  switch (source.type) {
+    case FLTSourceTypeCamera:
+      [self checkCameraAuthorizationWithImagePicker:imagePickerController
+                                             camera:[self cameraDeviceForSource:source]];
+      break;
+    case FLTSourceTypeGallery:
+      [self checkPhotoAuthorizationWithImagePicker:imagePickerController];
+      break;
+    default:
+      [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source"
+                                                        message:@"Invalid video source."
+                                                        details:nil]];
+      break;
+  }
+}
+
+#pragma mark -
+
+/**
+ * If a call is still in progress, cancels it by returning an error and then clearing state.
+ *
+ * TODO(stuartmorgan): Eliminate this, and instead track context per image picker (e.g., using
+ * associated objects).
+ */
+- (void)cancelInProgressCall {
+  if (self.callContext) {
+    [self sendCallResultWithError:[FlutterError errorWithCode:@"multiple_request"
+                                                      message:@"Cancelled by a second request"
+                                                      details:nil]];
+    self.callContext = nil;
+  }
+}
+
+- (void)showCamera:(UIImagePickerControllerCameraDevice)device
+    withImagePicker:(UIImagePickerController *)imagePickerController {
   @synchronized(self) {
     if (imagePickerController.beingPresented) {
       return;
     }
   }
-  UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments];
   // Camera is not available on simulators
   if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] &&
       [UIImagePickerController isCameraDeviceAvailable:device]) {
@@ -254,25 +291,24 @@
     [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert
                                                       animated:YES
                                                     completion:nil];
-    self.result(nil);
-    self.result = nil;
-    _arguments = nil;
+    [self sendCallResultWithSavedPathList:nil];
   }
 }
 
-- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController {
+- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController
+                                         camera:(UIImagePickerControllerCameraDevice)device {
   AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
 
   switch (status) {
     case AVAuthorizationStatusAuthorized:
-      [self showCameraWithImagePicker:imagePickerController];
+      [self showCamera:device withImagePicker:imagePickerController];
       break;
     case AVAuthorizationStatusNotDetermined: {
       [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
                                completionHandler:^(BOOL granted) {
                                  dispatch_async(dispatch_get_main_queue(), ^{
                                    if (granted) {
-                                     [self showCameraWithImagePicker:imagePickerController];
+                                     [self showCamera:device withImagePicker:imagePickerController];
                                    } else {
                                      [self errorNoCameraAccess:AVAuthorizationStatusDenied];
                                    }
@@ -352,15 +388,17 @@
 - (void)errorNoCameraAccess:(AVAuthorizationStatus)status {
   switch (status) {
     case AVAuthorizationStatusRestricted:
-      self.result([FlutterError errorWithCode:@"camera_access_restricted"
-                                      message:@"The user is not allowed to use the camera."
-                                      details:nil]);
+      [self sendCallResultWithError:[FlutterError
+                                        errorWithCode:@"camera_access_restricted"
+                                              message:@"The user is not allowed to use the camera."
+                                              details:nil]];
       break;
     case AVAuthorizationStatusDenied:
     default:
-      self.result([FlutterError errorWithCode:@"camera_access_denied"
-                                      message:@"The user did not allow camera access."
-                                      details:nil]);
+      [self sendCallResultWithError:[FlutterError
+                                        errorWithCode:@"camera_access_denied"
+                                              message:@"The user did not allow camera access."
+                                              details:nil]];
       break;
   }
 }
@@ -368,15 +406,17 @@
 - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status {
   switch (status) {
     case PHAuthorizationStatusRestricted:
-      self.result([FlutterError errorWithCode:@"photo_access_restricted"
-                                      message:@"The user is not allowed to use the photo."
-                                      details:nil]);
+      [self sendCallResultWithError:[FlutterError
+                                        errorWithCode:@"photo_access_restricted"
+                                              message:@"The user is not allowed to use the photo."
+                                              details:nil]];
       break;
     case PHAuthorizationStatusDenied:
     default:
-      self.result([FlutterError errorWithCode:@"photo_access_denied"
-                                      message:@"The user did not allow photo access."
-                                      details:nil]);
+      [self sendCallResultWithError:[FlutterError
+                                        errorWithCode:@"photo_access_denied"
+                                              message:@"The user did not allow photo access."
+                                              details:nil]];
       break;
   }
 }
@@ -406,31 +446,27 @@
   return imageQuality;
 }
 
+#pragma mark - UIAdaptivePresentationControllerDelegate
+
 - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
-  if (self.result != nil) {
-    self.result(nil);
-    self.result = nil;
-    self->_arguments = nil;
-  }
+  [self sendCallResultWithSavedPathList:nil];
 }
 
+#pragma mark - PHPickerViewControllerDelegate
+
 - (void)picker:(PHPickerViewController *)picker
     didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
   [picker dismissViewControllerAnimated:YES completion:nil];
   if (results.count == 0) {
-    if (self.result != nil) {
-      self.result(nil);
-      self.result = nil;
-      self->_arguments = nil;
-    }
+    [self sendCallResultWithSavedPathList:nil];
     return;
   }
   dispatch_queue_t backgroundQueue =
       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
   dispatch_async(backgroundQueue, ^{
-    NSNumber *maxWidth = GetNullableValueForKey(self->_arguments, @"maxWidth");
-    NSNumber *maxHeight = GetNullableValueForKey(self->_arguments, @"maxHeight");
-    NSNumber *imageQuality = GetNullableValueForKey(self->_arguments, @"imageQuality");
+    NSNumber *maxWidth = self.callContext.maxSize.width;
+    NSNumber *maxHeight = self.callContext.maxSize.height;
+    NSNumber *imageQuality = self.callContext.imageQuality;
     NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality];
     NSOperationQueue *operationQueue = [NSOperationQueue new];
     NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count];
@@ -449,11 +485,13 @@
     }
     [operationQueue waitUntilAllOperationsAreFinished];
     dispatch_async(dispatch_get_main_queue(), ^{
-      [self handleSavedPathList:pathList];
+      [self sendCallResultWithSavedPathList:pathList];
     });
   });
 }
 
+#pragma mark -
+
 /**
  * Creates an NSMutableArray of a certain size filled with NSNull objects.
  *
@@ -470,6 +508,8 @@
   return mutableArray;
 }
 
+#pragma mark - UIImagePickerControllerDelegate
+
 - (void)imagePickerController:(UIImagePickerController *)picker
     didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
   NSURL *videoURL = info[UIImagePickerControllerMediaURL];
@@ -478,7 +518,7 @@
   // further didFinishPickingMediaWithInfo invocations. A nil check is necessary
   // to prevent below code to be unwantly executed multiple times and cause a
   // crash.
-  if (!self.result) {
+  if (!self.callContext) {
     return;
   }
   if (videoURL != nil) {
@@ -493,27 +533,25 @@
           [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error];
 
           if (error) {
-            self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error"
-                                            message:@"Could not cache the video file."
-                                            details:nil]);
-            self.result = nil;
+            [self sendCallResultWithError:[FlutterError
+                                              errorWithCode:@"flutter_image_picker_copy_video_error"
+                                                    message:@"Could not cache the video file."
+                                                    details:nil]];
             return;
           }
         }
         videoURL = destination;
       }
     }
-    self.result(videoURL.path);
-    self.result = nil;
-    _arguments = nil;
+    [self sendCallResultWithSavedPathList:@[ videoURL.path ]];
   } else {
     UIImage *image = info[UIImagePickerControllerEditedImage];
     if (image == nil) {
       image = info[UIImagePickerControllerOriginalImage];
     }
-    NSNumber *maxWidth = GetNullableValueForKey(_arguments, @"maxWidth");
-    NSNumber *maxHeight = GetNullableValueForKey(_arguments, @"maxHeight");
-    NSNumber *imageQuality = GetNullableValueForKey(_arguments, @"imageQuality");
+    NSNumber *maxWidth = self.callContext.maxSize.width;
+    NSNumber *maxHeight = self.callContext.maxSize.height;
+    NSNumber *imageQuality = self.callContext.imageQuality;
     NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality];
 
     PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info];
@@ -547,14 +585,11 @@
 
 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
   [picker dismissViewControllerAnimated:YES completion:nil];
-  if (!self.result) {
-    return;
-  }
-  self.result(nil);
-  self.result = nil;
-  _arguments = nil;
+  [self sendCallResultWithSavedPathList:nil];
 }
 
+#pragma mark -
+
 - (void)saveImageWithOriginalImageData:(NSData *)originalImageData
                                  image:(UIImage *)image
                               maxWidth:(NSNumber *)maxWidth
@@ -566,7 +601,7 @@
                                                           maxWidth:maxWidth
                                                          maxHeight:maxHeight
                                                       imageQuality:imageQuality];
-  [self handleSavedPathList:@[ savedPath ]];
+  [self sendCallResultWithSavedPathList:@[ savedPath ]];
 }
 
 - (void)saveImageWithPickerInfo:(NSDictionary *)info
@@ -575,47 +610,36 @@
   NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info
                                                                         image:image
                                                                  imageQuality:imageQuality];
-  [self handleSavedPathList:@[ savedPath ]];
+  [self sendCallResultWithSavedPathList:@[ savedPath ]];
 }
 
-/**
- * Applies NSMutableArray on the FLutterResult.
- *
- * NSString must be returned by FlutterResult if the single image
- * mode is active. It is checked by maxImagesAllowed and
- * returns the first object of the pathlist.
- *
- * NSMutableArray must be returned by FlutterResult if the multi-image
- * mode is active. After the pathlist count is checked then it returns
- * the pathlist.
- *
- * @param pathList that should be applied to FlutterResult.
- */
-- (void)handleSavedPathList:(NSArray *)pathList {
-  if (!self.result) {
+- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList {
+  if (!self.callContext) {
     return;
   }
 
-  if (pathList) {
-    if (![pathList containsObject:[NSNull null]]) {
-      if ((self.maxImagesAllowed == 1)) {
-        self.result(pathList.firstObject);
-      } else {
-        self.result(pathList);
-      }
-    } else {
-      self.result([FlutterError errorWithCode:@"create_error"
-                                      message:@"pathList's items should not be null"
-                                      details:nil]);
-    }
+  if ([pathList containsObject:[NSNull null]]) {
+    self.callContext.result(nil, [FlutterError errorWithCode:@"create_error"
+                                                     message:@"pathList's items should not be null"
+                                                     details:nil]);
   } else {
-    // This should never happen.
-    self.result([FlutterError errorWithCode:@"create_error"
-                                    message:@"pathList should not be nil"
-                                    details:nil]);
+    self.callContext.result(pathList, nil);
   }
-  self.result = nil;
-  _arguments = nil;
+  self.callContext = nil;
+}
+
+/**
+ * Sends the given error via `callContext.result` as the result of the original platform channel
+ * method call, clearing the in-progress call state.
+ *
+ * @param error The error to return.
+ */
+- (void)sendCallResultWithError:(FlutterError *)error {
+  if (!self.callContext) {
+    return;
+  }
+  self.callContext.result(nil, error);
+  self.callContext = nil;
 }
 
 @end
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h
index 039f76d..2c41677 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h
@@ -6,26 +6,65 @@
 
 #import <image_picker_ios/FLTImagePickerPlugin.h>
 
-/** Methods exposed for unit testing. */
-@interface FLTImagePickerPlugin ()
+#import <messages.g.h>
 
-/** The Flutter result callback use to report results back to Flutter App. */
-@property(copy, nonatomic) FlutterResult result;
+NS_ASSUME_NONNULL_BEGIN
 
 /**
- * Applies NSMutableArray on the FLutterResult.
- *
- * NSString must be returned by FlutterResult if the single image
- * mode is active. It is checked by maxImagesAllowed and
- * returns the first object of the pathlist.
- *
- * NSMutableArray must be returned by FlutterResult if the multi-image
- * mode is active. After the pathlist count is checked then it returns
- * the pathlist.
- *
- * @param pathList that should be applied to FlutterResult.
+ * The return hander used for all method calls, which internally adapts the provided result list
+ * to return either a list or a single element depending on the original call.
  */
-- (void)handleSavedPathList:(NSArray *)pathList;
+typedef void (^FlutterResultAdapter)(NSArray<NSString *> *_Nullable, FlutterError *_Nullable);
+
+/**
+ * A container class for context to use when handling a method call from the Dart side.
+ */
+@interface FLTImagePickerMethodCallContext : NSObject
+
+/**
+ * Initializes a new context that calls |result| on completion of the operation.
+ */
+- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result;
+
+/** The callback to provide results to the Dart caller. */
+@property(nonatomic, copy, nonnull) FlutterResultAdapter result;
+
+/**
+ * The maximum size to enforce on the results.
+ *
+ * If nil, no resizing is done.
+ */
+@property(nonatomic, strong, nullable) FLTMaxSize *maxSize;
+
+/**
+ * The image quality to resample the results to.
+ *
+ * If nil, no resampling is done.
+ */
+@property(nonatomic, strong, nullable) NSNumber *imageQuality;
+
+/** Maximum number of images to select. 0 indicates no maximum. */
+@property(nonatomic, assign) int maxImageCount;
+
+@end
+
+#pragma mark -
+
+/** Methods exposed for unit testing. */
+@interface FLTImagePickerPlugin () <FLTImagePickerApi>
+
+/**
+ * The context of the Flutter method call that is currently being handled, if any.
+ */
+@property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext;
+
+/**
+ * Validates the provided paths list, then sends it via `callContext.result` as the result of the
+ * original platform channel method call, clearing the in-progress call state.
+ *
+ * @param pathList The paths to return. nil indicates a cancelled operation.
+ */
+- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList;
 
 /**
  * Tells the delegate that the user cancelled the pick operation.
@@ -52,3 +91,5 @@
     (NSArray<UIImagePickerController *> *)imagePickerControllers;
 
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h
new file mode 100644
index 0000000..310165f
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h
@@ -0,0 +1,61 @@
+// 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.
+// Autogenerated from Pigeon (v3.0.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@protocol FlutterMessageCodec;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSUInteger, FLTSourceCamera) {
+  FLTSourceCameraRear = 0,
+  FLTSourceCameraFront = 1,
+};
+
+typedef NS_ENUM(NSUInteger, FLTSourceType) {
+  FLTSourceTypeCamera = 0,
+  FLTSourceTypeGallery = 1,
+};
+
+@class FLTMaxSize;
+@class FLTSourceSpecification;
+
+@interface FLTMaxSize : NSObject
++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height;
+@property(nonatomic, strong, nullable) NSNumber *width;
+@property(nonatomic, strong, nullable) NSNumber *height;
+@end
+
+@interface FLTSourceSpecification : NSObject
+/// `init` unavailable to enforce nonnull fields, see the `make` class method.
+- (instancetype)init NS_UNAVAILABLE;
++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera;
+@property(nonatomic, assign) FLTSourceType type;
+@property(nonatomic, assign) FLTSourceCamera camera;
+@end
+
+/// The codec used by FLTImagePickerApi.
+NSObject<FlutterMessageCodec> *FLTImagePickerApiGetCodec(void);
+
+@protocol FLTImagePickerApi
+- (void)pickImageWithSource:(FLTSourceSpecification *)source
+                    maxSize:(FLTMaxSize *)maxSize
+                    quality:(nullable NSNumber *)imageQuality
+                 completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
+- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize
+                          quality:(nullable NSNumber *)imageQuality
+                       completion:(void (^)(NSArray<NSString *> *_Nullable,
+                                            FlutterError *_Nullable))completion;
+- (void)pickVideoWithSource:(FLTSourceSpecification *)source
+                maxDuration:(nullable NSNumber *)maxDurationSeconds
+                 completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
+@end
+
+extern void FLTImagePickerApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
+                                   NSObject<FLTImagePickerApi> *_Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m
new file mode 100644
index 0000000..6c91c0a
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m
@@ -0,0 +1,216 @@
+// 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.
+// Autogenerated from Pigeon (v3.0.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "messages.g.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary<NSString *, id> *wrapResult(id result, FlutterError *error) {
+  NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+  if (error) {
+    errorDict = @{
+      @"code" : (error.code ? error.code : [NSNull null]),
+      @"message" : (error.message ? error.message : [NSNull null]),
+      @"details" : (error.details ? error.details : [NSNull null]),
+    };
+  }
+  return @{
+    @"result" : (result ? result : [NSNull null]),
+    @"error" : errorDict,
+  };
+}
+static id GetNullableObject(NSDictionary *dict, id key) {
+  id result = dict[key];
+  return (result == [NSNull null]) ? nil : result;
+}
+static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
+  id result = array[key];
+  return (result == [NSNull null]) ? nil : result;
+}
+
+@interface FLTMaxSize ()
++ (FLTMaxSize *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+@interface FLTSourceSpecification ()
++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+
+@implementation FLTMaxSize
++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height {
+  FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init];
+  pigeonResult.width = width;
+  pigeonResult.height = height;
+  return pigeonResult;
+}
++ (FLTMaxSize *)fromMap:(NSDictionary *)dict {
+  FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init];
+  pigeonResult.width = GetNullableObject(dict, @"width");
+  pigeonResult.height = GetNullableObject(dict, @"height");
+  return pigeonResult;
+}
+- (NSDictionary *)toMap {
+  return [NSDictionary
+      dictionaryWithObjectsAndKeys:(self.width ? self.width : [NSNull null]), @"width",
+                                   (self.height ? self.height : [NSNull null]), @"height", nil];
+}
+@end
+
+@implementation FLTSourceSpecification
++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera {
+  FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init];
+  pigeonResult.type = type;
+  pigeonResult.camera = camera;
+  return pigeonResult;
+}
++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict {
+  FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init];
+  pigeonResult.type = [GetNullableObject(dict, @"type") integerValue];
+  pigeonResult.camera = [GetNullableObject(dict, @"camera") integerValue];
+  return pigeonResult;
+}
+- (NSDictionary *)toMap {
+  return [NSDictionary
+      dictionaryWithObjectsAndKeys:@(self.type), @"type", @(self.camera), @"camera", nil];
+}
+@end
+
+@interface FLTImagePickerApiCodecReader : FlutterStandardReader
+@end
+@implementation FLTImagePickerApiCodecReader
+- (nullable id)readValueOfType:(UInt8)type {
+  switch (type) {
+    case 128:
+      return [FLTMaxSize fromMap:[self readValue]];
+
+    case 129:
+      return [FLTSourceSpecification fromMap:[self readValue]];
+
+    default:
+      return [super readValueOfType:type];
+  }
+}
+@end
+
+@interface FLTImagePickerApiCodecWriter : FlutterStandardWriter
+@end
+@implementation FLTImagePickerApiCodecWriter
+- (void)writeValue:(id)value {
+  if ([value isKindOfClass:[FLTMaxSize class]]) {
+    [self writeByte:128];
+    [self writeValue:[value toMap]];
+  } else if ([value isKindOfClass:[FLTSourceSpecification class]]) {
+    [self writeByte:129];
+    [self writeValue:[value toMap]];
+  } else {
+    [super writeValue:value];
+  }
+}
+@end
+
+@interface FLTImagePickerApiCodecReaderWriter : FlutterStandardReaderWriter
+@end
+@implementation FLTImagePickerApiCodecReaderWriter
+- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
+  return [[FLTImagePickerApiCodecWriter alloc] initWithData:data];
+}
+- (FlutterStandardReader *)readerWithData:(NSData *)data {
+  return [[FLTImagePickerApiCodecReader alloc] initWithData:data];
+}
+@end
+
+NSObject<FlutterMessageCodec> *FLTImagePickerApiGetCodec() {
+  static dispatch_once_t sPred = 0;
+  static FlutterStandardMessageCodec *sSharedObject = nil;
+  dispatch_once(&sPred, ^{
+    FLTImagePickerApiCodecReaderWriter *readerWriter =
+        [[FLTImagePickerApiCodecReaderWriter alloc] init];
+    sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
+  });
+  return sSharedObject;
+}
+
+void FLTImagePickerApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
+                            NSObject<FLTImagePickerApi> *api) {
+  {
+    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
+           initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickImage"
+        binaryMessenger:binaryMessenger
+                  codec:FLTImagePickerApiGetCodec()];
+    if (api) {
+      NSCAssert([api respondsToSelector:@selector(pickImageWithSource:maxSize:quality:completion:)],
+                @"FLTImagePickerApi api (%@) doesn't respond to "
+                @"@selector(pickImageWithSource:maxSize:quality:completion:)",
+                api);
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        NSArray *args = message;
+        FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0);
+        FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 1);
+        NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 2);
+        [api pickImageWithSource:arg_source
+                         maxSize:arg_maxSize
+                         quality:arg_imageQuality
+                      completion:^(NSString *_Nullable output, FlutterError *_Nullable error) {
+                        callback(wrapResult(output, error));
+                      }];
+      }];
+    } else {
+      [channel setMessageHandler:nil];
+    }
+  }
+  {
+    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
+           initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMultiImage"
+        binaryMessenger:binaryMessenger
+                  codec:FLTImagePickerApiGetCodec()];
+    if (api) {
+      NSCAssert([api respondsToSelector:@selector(pickMultiImageWithMaxSize:quality:completion:)],
+                @"FLTImagePickerApi api (%@) doesn't respond to "
+                @"@selector(pickMultiImageWithMaxSize:quality:completion:)",
+                api);
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        NSArray *args = message;
+        FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0);
+        NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1);
+        [api pickMultiImageWithMaxSize:arg_maxSize
+                               quality:arg_imageQuality
+                            completion:^(NSArray<NSString *> *_Nullable output,
+                                         FlutterError *_Nullable error) {
+                              callback(wrapResult(output, error));
+                            }];
+      }];
+    } else {
+      [channel setMessageHandler:nil];
+    }
+  }
+  {
+    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
+           initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickVideo"
+        binaryMessenger:binaryMessenger
+                  codec:FLTImagePickerApiGetCodec()];
+    if (api) {
+      NSCAssert([api respondsToSelector:@selector(pickVideoWithSource:maxDuration:completion:)],
+                @"FLTImagePickerApi api (%@) doesn't respond to "
+                @"@selector(pickVideoWithSource:maxDuration:completion:)",
+                api);
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        NSArray *args = message;
+        FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0);
+        NSNumber *arg_maxDurationSeconds = GetNullableObjectAtIndex(args, 1);
+        [api pickVideoWithSource:arg_source
+                     maxDuration:arg_maxDurationSeconds
+                      completion:^(NSString *_Nullable output, FlutterError *_Nullable error) {
+                        callback(wrapResult(output, error));
+                      }];
+      }];
+    } else {
+      [channel setMessageHandler:nil];
+    }
+  }
+}
diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart
new file mode 100644
index 0000000..3d1413c
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart
@@ -0,0 +1,209 @@
+// 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 'dart:async';
+
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+
+import 'src/messages.g.dart';
+
+// Converts an [ImageSource] to the corresponding Pigeon API enum value.
+SourceType _convertSource(ImageSource source) {
+  switch (source) {
+    case ImageSource.camera:
+      return SourceType.camera;
+    case ImageSource.gallery:
+      return SourceType.gallery;
+    default:
+      throw UnimplementedError('Unknown source: $source');
+  }
+}
+
+// Converts a [CameraDevice] to the corresponding Pigeon API enum value.
+SourceCamera _convertCamera(CameraDevice camera) {
+  switch (camera) {
+    case CameraDevice.front:
+      return SourceCamera.front;
+    case CameraDevice.rear:
+      return SourceCamera.rear;
+    default:
+      throw UnimplementedError('Unknown camera: $camera');
+  }
+}
+
+/// An implementation of [ImagePickerPlatform] for iOS.
+class ImagePickerIOS extends ImagePickerPlatform {
+  final ImagePickerApi _hostApi = ImagePickerApi();
+
+  /// Registers this class as the default platform implementation.
+  static void registerWith() {
+    ImagePickerPlatform.instance = ImagePickerIOS();
+  }
+
+  @override
+  Future<PickedFile?> pickImage({
+    required ImageSource source,
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+  }) async {
+    final String? path = await _pickImageAsPath(
+      source: source,
+      maxWidth: maxWidth,
+      maxHeight: maxHeight,
+      imageQuality: imageQuality,
+      preferredCameraDevice: preferredCameraDevice,
+    );
+    return path != null ? PickedFile(path) : null;
+  }
+
+  @override
+  Future<List<PickedFile>?> pickMultiImage({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) async {
+    final List<dynamic>? paths = await _pickMultiImageAsPath(
+      maxWidth: maxWidth,
+      maxHeight: maxHeight,
+      imageQuality: imageQuality,
+    );
+    if (paths == null) {
+      return null;
+    }
+
+    return paths.map((dynamic path) => PickedFile(path as String)).toList();
+  }
+
+  Future<List<String>?> _pickMultiImageAsPath({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) async {
+    if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
+      throw ArgumentError.value(
+          imageQuality, 'imageQuality', 'must be between 0 and 100');
+    }
+
+    if (maxWidth != null && maxWidth < 0) {
+      throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
+    }
+
+    if (maxHeight != null && maxHeight < 0) {
+      throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
+    }
+
+    // TODO(stuartmorgan): Remove the cast once Pigeon supports non-nullable
+    //  generics, https://github.com/flutter/flutter/issues/97848
+    return (await _hostApi.pickMultiImage(
+            MaxSize(width: maxWidth, height: maxHeight), imageQuality))
+        ?.cast<String>();
+  }
+
+  Future<String?> _pickImageAsPath({
+    required ImageSource source,
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+  }) {
+    if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
+      throw ArgumentError.value(
+          imageQuality, 'imageQuality', 'must be between 0 and 100');
+    }
+
+    if (maxWidth != null && maxWidth < 0) {
+      throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
+    }
+
+    if (maxHeight != null && maxHeight < 0) {
+      throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
+    }
+
+    return _hostApi.pickImage(
+      SourceSpecification(
+          type: _convertSource(source),
+          camera: _convertCamera(preferredCameraDevice)),
+      MaxSize(width: maxWidth, height: maxHeight),
+      imageQuality,
+    );
+  }
+
+  @override
+  Future<PickedFile?> pickVideo({
+    required ImageSource source,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+    Duration? maxDuration,
+  }) async {
+    final String? path = await _pickVideoAsPath(
+      source: source,
+      maxDuration: maxDuration,
+      preferredCameraDevice: preferredCameraDevice,
+    );
+    return path != null ? PickedFile(path) : null;
+  }
+
+  Future<String?> _pickVideoAsPath({
+    required ImageSource source,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+    Duration? maxDuration,
+  }) {
+    return _hostApi.pickVideo(
+        SourceSpecification(
+            type: _convertSource(source),
+            camera: _convertCamera(preferredCameraDevice)),
+        maxDuration?.inSeconds);
+  }
+
+  @override
+  Future<XFile?> getImage({
+    required ImageSource source,
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+  }) async {
+    final String? path = await _pickImageAsPath(
+      source: source,
+      maxWidth: maxWidth,
+      maxHeight: maxHeight,
+      imageQuality: imageQuality,
+      preferredCameraDevice: preferredCameraDevice,
+    );
+    return path != null ? XFile(path) : null;
+  }
+
+  @override
+  Future<List<XFile>?> getMultiImage({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) async {
+    final List<String>? paths = await _pickMultiImageAsPath(
+      maxWidth: maxWidth,
+      maxHeight: maxHeight,
+      imageQuality: imageQuality,
+    );
+    if (paths == null) {
+      return null;
+    }
+
+    return paths.map((String path) => XFile(path)).toList();
+  }
+
+  @override
+  Future<XFile?> getVideo({
+    required ImageSource source,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+    Duration? maxDuration,
+  }) async {
+    final String? path = await _pickVideoAsPath(
+      source: source,
+      maxDuration: maxDuration,
+      preferredCameraDevice: preferredCameraDevice,
+    );
+    return path != null ? XFile(path) : null;
+  }
+}
diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart
new file mode 100644
index 0000000..0c5859e
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart
@@ -0,0 +1,194 @@
+// 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.
+// Autogenerated from Pigeon (v3.0.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+
+enum SourceCamera {
+  rear,
+  front,
+}
+
+enum SourceType {
+  camera,
+  gallery,
+}
+
+class MaxSize {
+  MaxSize({
+    this.width,
+    this.height,
+  });
+
+  double? width;
+  double? height;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['width'] = width;
+    pigeonMap['height'] = height;
+    return pigeonMap;
+  }
+
+  static MaxSize decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return MaxSize(
+      width: pigeonMap['width'] as double?,
+      height: pigeonMap['height'] as double?,
+    );
+  }
+}
+
+class SourceSpecification {
+  SourceSpecification({
+    required this.type,
+    this.camera,
+  });
+
+  SourceType type;
+  SourceCamera? camera;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['type'] = type.index;
+    pigeonMap['camera'] = camera?.index;
+    return pigeonMap;
+  }
+
+  static SourceSpecification decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SourceSpecification(
+      type: SourceType.values[pigeonMap['type']! as int],
+      camera: pigeonMap['camera'] != null
+          ? SourceCamera.values[pigeonMap['camera']! as int]
+          : null,
+    );
+  }
+}
+
+class _ImagePickerApiCodec extends StandardMessageCodec {
+  const _ImagePickerApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is MaxSize) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is SourceSpecification) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return MaxSize.decode(readValue(buffer)!);
+
+      case 129:
+        return SourceSpecification.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+class ImagePickerApi {
+  /// Constructor for [ImagePickerApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  ImagePickerApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _ImagePickerApiCodec();
+
+  Future<String?> pickImage(SourceSpecification arg_source, MaxSize arg_maxSize,
+      int? arg_imageQuality) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.ImagePickerApi.pickImage', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_source, arg_maxSize, arg_imageQuality])
+            as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return (replyMap['result'] as String?);
+    }
+  }
+
+  Future<List<String?>?> pickMultiImage(
+      MaxSize arg_maxSize, int? arg_imageQuality) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_maxSize, arg_imageQuality])
+            as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return (replyMap['result'] as List<Object?>?)?.cast<String?>();
+    }
+  }
+
+  Future<String?> pickVideo(
+      SourceSpecification arg_source, int? arg_maxDurationSeconds) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_source, arg_maxDurationSeconds])
+            as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return (replyMap['result'] as String?);
+    }
+  }
+}
diff --git a/packages/image_picker/image_picker_ios/pigeons/copyright.txt b/packages/image_picker/image_picker_ios/pigeons/copyright.txt
new file mode 100644
index 0000000..1236b63
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/pigeons/copyright.txt
@@ -0,0 +1,3 @@
+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.
diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart
new file mode 100644
index 0000000..94ac034
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart
@@ -0,0 +1,47 @@
+// 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 'package:pigeon/pigeon.dart';
+
+@ConfigurePigeon(PigeonOptions(
+  dartOut: 'lib/src/messages.g.dart',
+  dartTestOut: 'test/test_api.dart',
+  objcHeaderOut: 'ios/Classes/messages.g.h',
+  objcSourceOut: 'ios/Classes/messages.g.m',
+  objcOptions: ObjcOptions(
+    prefix: 'FLT',
+  ),
+  copyrightHeader: 'pigeons/copyright.txt',
+))
+class MaxSize {
+  MaxSize(this.width, this.height);
+  double? width;
+  double? height;
+}
+
+// Corresponds to `CameraDevice` from the platform interface package.
+enum SourceCamera { rear, front }
+
+// Corresponds to `ImageSource` from the platform interface package.
+enum SourceType { camera, gallery }
+
+class SourceSpecification {
+  SourceSpecification(this.type, this.camera);
+  SourceType type;
+  SourceCamera? camera;
+}
+
+@HostApi(dartHostTestHandler: 'TestHostImagePickerApi')
+abstract class ImagePickerApi {
+  @async
+  @ObjCSelector('pickImageWithSource:maxSize:quality:')
+  String? pickImage(
+      SourceSpecification source, MaxSize maxSize, int? imageQuality);
+  @async
+  @ObjCSelector('pickMultiImageWithMaxSize:quality:')
+  List<String>? pickMultiImage(MaxSize maxSize, int? imageQuality);
+  @async
+  @ObjCSelector('pickVideoWithSource:maxDuration:')
+  String? pickVideo(SourceSpecification source, int? maxDurationSeconds);
+}
diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml
index 2587c9a..a9cd052 100755
--- a/packages/image_picker/image_picker_ios/pubspec.yaml
+++ b/packages/image_picker/image_picker_ios/pubspec.yaml
@@ -2,17 +2,18 @@
 description: iOS implementation of the video_picker plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_ios
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 0.8.4+11
+version: 0.8.5
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
-  flutter: ">=2.5.0"
+  flutter: ">=2.8.0"
 
 flutter:
   plugin:
     implements: image_picker
     platforms:
       ios:
+        dartPluginClass: ImagePickerIOS
         pluginClass: FLTImagePickerPlugin
 
 dependencies:
@@ -24,3 +25,4 @@
   flutter_test:
     sdk: flutter
   mockito: ^5.0.0
+  pigeon: ^3.0.2
diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart
new file mode 100644
index 0000000..09517f1
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart
@@ -0,0 +1,937 @@
+// 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 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:image_picker_ios/image_picker_ios.dart';
+import 'package:image_picker_ios/src/messages.g.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+
+import 'test_api.dart';
+
+@immutable
+class _LoggedMethodCall {
+  const _LoggedMethodCall(this.name, {required this.arguments});
+  final String name;
+  final Map<String, Object?> arguments;
+
+  @override
+  bool operator ==(Object other) {
+    return other is _LoggedMethodCall &&
+        name == other.name &&
+        mapEquals(arguments, other.arguments);
+  }
+
+  @override
+  int get hashCode => Object.hash(name, arguments);
+
+  @override
+  String toString() {
+    return 'MethodCall: $name $arguments';
+  }
+}
+
+class _ApiLogger implements TestHostImagePickerApi {
+  // The value to return from future calls.
+  dynamic returnValue = '';
+  final List<_LoggedMethodCall> calls = <_LoggedMethodCall>[];
+
+  @override
+  Future<String?> pickImage(
+      SourceSpecification source, MaxSize maxSize, int? imageQuality) async {
+    // Flatten arguments for easy comparison.
+    calls.add(_LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+      'source': source.type,
+      'cameraDevice': source.camera,
+      'maxWidth': maxSize.width,
+      'maxHeight': maxSize.height,
+      'imageQuality': imageQuality,
+    }));
+    return returnValue as String?;
+  }
+
+  @override
+  Future<List<String?>?> pickMultiImage(
+      MaxSize maxSize, int? imageQuality) async {
+    calls.add(_LoggedMethodCall('pickMultiImage', arguments: <String, dynamic>{
+      'maxWidth': maxSize.width,
+      'maxHeight': maxSize.height,
+      'imageQuality': imageQuality,
+    }));
+    return returnValue as List<String?>?;
+  }
+
+  @override
+  Future<String?> pickVideo(
+      SourceSpecification source, int? maxDurationSeconds) async {
+    calls.add(_LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+      'source': source.type,
+      'cameraDevice': source.camera,
+      'maxDuration': maxDurationSeconds,
+    }));
+    return returnValue as String?;
+  }
+}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final ImagePickerIOS picker = ImagePickerIOS();
+  late _ApiLogger log;
+
+  setUp(() {
+    log = _ApiLogger();
+    TestHostImagePickerApi.setup(log);
+  });
+
+  test('registration', () async {
+    ImagePickerIOS.registerWith();
+    expect(ImagePickerPlatform.instance, isA<ImagePickerIOS>());
+  });
+
+  group('#pickImage', () {
+    test('passes the image source argument correctly', () async {
+      await picker.pickImage(source: ImageSource.camera);
+      await picker.pickImage(source: ImageSource.gallery);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.gallery,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+        ],
+      );
+    });
+
+    test('passes the width and height arguments correctly', () async {
+      await picker.pickImage(source: ImageSource.camera);
+      await picker.pickImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+      );
+      await picker.pickImage(
+        source: ImageSource.camera,
+        maxHeight: 10.0,
+      );
+      await picker.pickImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+      );
+      await picker.pickImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+        imageQuality: 70,
+      );
+      await picker.pickImage(
+        source: ImageSource.camera,
+        maxHeight: 10.0,
+        imageQuality: 70,
+      );
+      await picker.pickImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+        imageQuality: 70,
+      );
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': 10.0,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': 20.0,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': null,
+            'imageQuality': 70,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': 10.0,
+            'imageQuality': 70,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': 20.0,
+            'imageQuality': 70,
+            'cameraDevice': SourceCamera.rear
+          }),
+        ],
+      );
+    });
+
+    test('does not accept a invalid imageQuality argument', () {
+      expect(
+        () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.pickImage(imageQuality: 101, source: ImageSource.gallery),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.pickImage(imageQuality: -1, source: ImageSource.camera),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.pickImage(imageQuality: 101, source: ImageSource.camera),
+        throwsArgumentError,
+      );
+    });
+
+    test('does not accept a negative width or height argument', () {
+      expect(
+        () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0),
+        throwsArgumentError,
+      );
+    });
+
+    test('handles a null image path response gracefully', () async {
+      log.returnValue = null;
+
+      expect(await picker.pickImage(source: ImageSource.gallery), isNull);
+      expect(await picker.pickImage(source: ImageSource.camera), isNull);
+    });
+
+    test('camera position defaults to back', () async {
+      await picker.pickImage(source: ImageSource.camera);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear,
+          }),
+        ],
+      );
+    });
+
+    test('camera position can set to front', () async {
+      await picker.pickImage(
+          source: ImageSource.camera,
+          preferredCameraDevice: CameraDevice.front);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.front,
+          }),
+        ],
+      );
+    });
+  });
+
+  group('#pickMultiImage', () {
+    test('calls the method correctly', () async {
+      log.returnValue = <String>['0', '1'];
+      await picker.pickMultiImage();
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+        ],
+      );
+    });
+
+    test('passes the width and height arguments correctly', () async {
+      log.returnValue = <String>['0', '1'];
+      await picker.pickMultiImage();
+      await picker.pickMultiImage(
+        maxWidth: 10.0,
+      );
+      await picker.pickMultiImage(
+        maxHeight: 10.0,
+      );
+      await picker.pickMultiImage(
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+      );
+      await picker.pickMultiImage(
+        maxWidth: 10.0,
+        imageQuality: 70,
+      );
+      await picker.pickMultiImage(
+        maxHeight: 10.0,
+        imageQuality: 70,
+      );
+      await picker.pickMultiImage(
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+        imageQuality: 70,
+      );
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': 70,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': 70,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': 70,
+              }),
+        ],
+      );
+    });
+
+    test('does not accept a negative width or height argument', () {
+      expect(
+        () => picker.pickMultiImage(maxWidth: -1.0),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.pickMultiImage(maxHeight: -1.0),
+        throwsArgumentError,
+      );
+    });
+
+    test('does not accept a invalid imageQuality argument', () {
+      expect(
+        () => picker.pickMultiImage(imageQuality: -1),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.pickMultiImage(imageQuality: 101),
+        throwsArgumentError,
+      );
+    });
+
+    test('handles a null image path response gracefully', () async {
+      log.returnValue = null;
+
+      expect(await picker.pickMultiImage(), isNull);
+    });
+  });
+
+  group('#pickVideo', () {
+    test('passes the image source argument correctly', () async {
+      await picker.pickVideo(source: ImageSource.camera);
+      await picker.pickVideo(source: ImageSource.gallery);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'cameraDevice': SourceCamera.rear,
+            'maxDuration': null,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.gallery,
+            'cameraDevice': SourceCamera.rear,
+            'maxDuration': null,
+          }),
+        ],
+      );
+    });
+
+    test('passes the duration argument correctly', () async {
+      await picker.pickVideo(source: ImageSource.camera);
+      await picker.pickVideo(
+        source: ImageSource.camera,
+        maxDuration: const Duration(seconds: 10),
+      );
+      await picker.pickVideo(
+        source: ImageSource.camera,
+        maxDuration: const Duration(minutes: 1),
+      );
+      await picker.pickVideo(
+        source: ImageSource.camera,
+        maxDuration: const Duration(hours: 1),
+      );
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': null,
+            'cameraDevice': SourceCamera.rear,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': 10,
+            'cameraDevice': SourceCamera.rear,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': 60,
+            'cameraDevice': SourceCamera.rear,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': 3600,
+            'cameraDevice': SourceCamera.rear,
+          }),
+        ],
+      );
+    });
+
+    test('handles a null video path response gracefully', () async {
+      log.returnValue = null;
+
+      expect(await picker.pickVideo(source: ImageSource.gallery), isNull);
+      expect(await picker.pickVideo(source: ImageSource.camera), isNull);
+    });
+
+    test('camera position defaults to back', () async {
+      await picker.pickVideo(source: ImageSource.camera);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'cameraDevice': SourceCamera.rear,
+            'maxDuration': null,
+          }),
+        ],
+      );
+    });
+
+    test('camera position can set to front', () async {
+      await picker.pickVideo(
+        source: ImageSource.camera,
+        preferredCameraDevice: CameraDevice.front,
+      );
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': null,
+            'cameraDevice': SourceCamera.front,
+          }),
+        ],
+      );
+    });
+  });
+
+  group('#getImage', () {
+    test('passes the image source argument correctly', () async {
+      await picker.getImage(source: ImageSource.camera);
+      await picker.getImage(source: ImageSource.gallery);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.gallery,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+        ],
+      );
+    });
+
+    test('passes the width and height arguments correctly', () async {
+      await picker.getImage(source: ImageSource.camera);
+      await picker.getImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+      );
+      await picker.getImage(
+        source: ImageSource.camera,
+        maxHeight: 10.0,
+      );
+      await picker.getImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+      );
+      await picker.getImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+        imageQuality: 70,
+      );
+      await picker.getImage(
+        source: ImageSource.camera,
+        maxHeight: 10.0,
+        imageQuality: 70,
+      );
+      await picker.getImage(
+        source: ImageSource.camera,
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+        imageQuality: 70,
+      );
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': 10.0,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': 20.0,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': null,
+            'imageQuality': 70,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': 10.0,
+            'imageQuality': 70,
+            'cameraDevice': SourceCamera.rear
+          }),
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': 10.0,
+            'maxHeight': 20.0,
+            'imageQuality': 70,
+            'cameraDevice': SourceCamera.rear
+          }),
+        ],
+      );
+    });
+
+    test('does not accept a invalid imageQuality argument', () {
+      expect(
+        () => picker.getImage(imageQuality: -1, source: ImageSource.gallery),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.getImage(imageQuality: 101, source: ImageSource.gallery),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.getImage(imageQuality: -1, source: ImageSource.camera),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.getImage(imageQuality: 101, source: ImageSource.camera),
+        throwsArgumentError,
+      );
+    });
+
+    test('does not accept a negative width or height argument', () {
+      expect(
+        () => picker.getImage(source: ImageSource.camera, maxWidth: -1.0),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.getImage(source: ImageSource.camera, maxHeight: -1.0),
+        throwsArgumentError,
+      );
+    });
+
+    test('handles a null image path response gracefully', () async {
+      log.returnValue = null;
+
+      expect(await picker.getImage(source: ImageSource.gallery), isNull);
+      expect(await picker.getImage(source: ImageSource.camera), isNull);
+    });
+
+    test('camera position defaults to back', () async {
+      await picker.getImage(source: ImageSource.camera);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.rear,
+          }),
+        ],
+      );
+    });
+
+    test('camera position can set to front', () async {
+      await picker.getImage(
+          source: ImageSource.camera,
+          preferredCameraDevice: CameraDevice.front);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickImage', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxWidth': null,
+            'maxHeight': null,
+            'imageQuality': null,
+            'cameraDevice': SourceCamera.front,
+          }),
+        ],
+      );
+    });
+  });
+
+  group('#getMultiImage', () {
+    test('calls the method correctly', () async {
+      log.returnValue = <String>['0', '1'];
+      await picker.getMultiImage();
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+        ],
+      );
+    });
+
+    test('passes the width and height arguments correctly', () async {
+      log.returnValue = <String>['0', '1'];
+      await picker.getMultiImage();
+      await picker.getMultiImage(
+        maxWidth: 10.0,
+      );
+      await picker.getMultiImage(
+        maxHeight: 10.0,
+      );
+      await picker.getMultiImage(
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+      );
+      await picker.getMultiImage(
+        maxWidth: 10.0,
+        imageQuality: 70,
+      );
+      await picker.getMultiImage(
+        maxHeight: 10.0,
+        imageQuality: 70,
+      );
+      await picker.getMultiImage(
+        maxWidth: 10.0,
+        maxHeight: 20.0,
+        imageQuality: 70,
+      );
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': null,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': 70,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': 70,
+              }),
+          const _LoggedMethodCall('pickMultiImage',
+              arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': 70,
+              }),
+        ],
+      );
+    });
+
+    test('does not accept a negative width or height argument', () {
+      log.returnValue = <String>['0', '1'];
+      expect(
+        () => picker.getMultiImage(maxWidth: -1.0),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.getMultiImage(maxHeight: -1.0),
+        throwsArgumentError,
+      );
+    });
+
+    test('does not accept a invalid imageQuality argument', () {
+      log.returnValue = <String>['0', '1'];
+      expect(
+        () => picker.getMultiImage(imageQuality: -1),
+        throwsArgumentError,
+      );
+
+      expect(
+        () => picker.getMultiImage(imageQuality: 101),
+        throwsArgumentError,
+      );
+    });
+
+    test('handles a null image path response gracefully', () async {
+      log.returnValue = null;
+
+      expect(await picker.getMultiImage(), isNull);
+      expect(await picker.getMultiImage(), isNull);
+    });
+  });
+
+  group('#getVideo', () {
+    test('passes the image source argument correctly', () async {
+      await picker.getVideo(source: ImageSource.camera);
+      await picker.getVideo(source: ImageSource.gallery);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'cameraDevice': SourceCamera.rear,
+            'maxDuration': null,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.gallery,
+            'cameraDevice': SourceCamera.rear,
+            'maxDuration': null,
+          }),
+        ],
+      );
+    });
+
+    test('passes the duration argument correctly', () async {
+      await picker.getVideo(source: ImageSource.camera);
+      await picker.getVideo(
+        source: ImageSource.camera,
+        maxDuration: const Duration(seconds: 10),
+      );
+      await picker.getVideo(
+        source: ImageSource.camera,
+        maxDuration: const Duration(minutes: 1),
+      );
+      await picker.getVideo(
+        source: ImageSource.camera,
+        maxDuration: const Duration(hours: 1),
+      );
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': null,
+            'cameraDevice': SourceCamera.rear,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': 10,
+            'cameraDevice': SourceCamera.rear,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': 60,
+            'cameraDevice': SourceCamera.rear,
+          }),
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': 3600,
+            'cameraDevice': SourceCamera.rear,
+          }),
+        ],
+      );
+    });
+
+    test('handles a null video path response gracefully', () async {
+      log.returnValue = null;
+
+      expect(await picker.getVideo(source: ImageSource.gallery), isNull);
+      expect(await picker.getVideo(source: ImageSource.camera), isNull);
+    });
+
+    test('camera position defaults to back', () async {
+      await picker.getVideo(source: ImageSource.camera);
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'cameraDevice': SourceCamera.rear,
+            'maxDuration': null,
+          }),
+        ],
+      );
+    });
+
+    test('camera position can set to front', () async {
+      await picker.getVideo(
+        source: ImageSource.camera,
+        preferredCameraDevice: CameraDevice.front,
+      );
+
+      expect(
+        log.calls,
+        <_LoggedMethodCall>[
+          const _LoggedMethodCall('pickVideo', arguments: <String, dynamic>{
+            'source': SourceType.camera,
+            'maxDuration': null,
+            'cameraDevice': SourceCamera.front,
+          }),
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.dart
new file mode 100644
index 0000000..1f76e87
--- /dev/null
+++ b/packages/image_picker/image_picker_ios/test/test_api.dart
@@ -0,0 +1,127 @@
+// 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.
+// Autogenerated from Pigeon (v3.0.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis
+// ignore_for_file: avoid_relative_lib_imports
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+// Manually changed due to https://github.com/flutter/flutter/issues/97744
+import 'package:image_picker_ios/src/messages.g.dart';
+
+class _TestHostImagePickerApiCodec extends StandardMessageCodec {
+  const _TestHostImagePickerApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is MaxSize) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is SourceSpecification) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return MaxSize.decode(readValue(buffer)!);
+
+      case 129:
+        return SourceSpecification.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+abstract class TestHostImagePickerApi {
+  static const MessageCodec<Object?> codec = _TestHostImagePickerApiCodec();
+
+  Future<String?> pickImage(
+      SourceSpecification source, MaxSize maxSize, int? imageQuality);
+  Future<List<String?>?> pickMultiImage(MaxSize maxSize, int? imageQuality);
+  Future<String?> pickVideo(
+      SourceSpecification source, int? maxDurationSeconds);
+  static void setup(TestHostImagePickerApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.ImagePickerApi.pickImage', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final SourceSpecification? arg_source =
+              (args[0] as SourceSpecification?);
+          assert(arg_source != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null SourceSpecification.');
+          final MaxSize? arg_maxSize = (args[1] as MaxSize?);
+          assert(arg_maxSize != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null MaxSize.');
+          final int? arg_imageQuality = (args[2] as int?);
+          final String? output =
+              await api.pickImage(arg_source!, arg_maxSize!, arg_imageQuality);
+          return <Object?, Object?>{'result': output};
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final MaxSize? arg_maxSize = (args[0] as MaxSize?);
+          assert(arg_maxSize != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null, expected non-null MaxSize.');
+          final int? arg_imageQuality = (args[1] as int?);
+          final List<String?>? output =
+              await api.pickMultiImage(arg_maxSize!, arg_imageQuality);
+          return <Object?, Object?>{'result': output};
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final SourceSpecification? arg_source =
+              (args[0] as SourceSpecification?);
+          assert(arg_source != null,
+              'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null, expected non-null SourceSpecification.');
+          final int? arg_maxDurationSeconds = (args[1] as int?);
+          final String? output =
+              await api.pickVideo(arg_source!, arg_maxDurationSeconds);
+          return <Object?, Object?>{'result': output};
+        });
+      }
+    }
+  }
+}