[image_picker_ios] Pass through error message from image saving (#6858)

* [image_picker_ios] Pass through error message from image saving

* Review edits

* Format

* addObject
diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md
index bff6dd7..f51f46c 100644
--- a/packages/image_picker/image_picker_ios/CHANGELOG.md
+++ b/packages/image_picker/image_picker_ios/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.6+3
+
+* Returns error on image load failure.
+
 ## 0.8.6+2
 
 * Fixes issue where selectable images of certain types (such as ProRAW images) could not be loaded.
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 320582b..14491b2 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
@@ -6,7 +6,9 @@
 
 @import image_picker_ios;
 @import image_picker_ios.Test;
+@import UniformTypeIdentifiers;
 @import XCTest;
+
 #import <OCMock/OCMock.h>
 
 @interface MockViewController : UIViewController
@@ -269,37 +271,130 @@
 - (void)testPluginMultiImagePathHasNullItem {
   FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
 
-  dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
-  __block FlutterError *pickImageResult = nil;
+  XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
   plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
       initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
-        pickImageResult = error;
-        dispatch_semaphore_signal(resultSemaphore);
+        XCTAssertEqualObjects(error.code, @"create_error");
+        [resultExpectation fulfill];
       }];
   [plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]];
 
-  dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
-
-  XCTAssertEqualObjects(pickImageResult.code, @"create_error");
+  [self waitForExpectationsWithTimeout:30 handler:nil];
 }
 
 - (void)testPluginMultiImagePathHasItem {
   FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
   NSArray *pathList = @[ @"test" ];
 
-  dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
-  __block id pickImageResult = nil;
+  XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
 
   plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
       initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
-        pickImageResult = result;
-        dispatch_semaphore_signal(resultSemaphore);
+        XCTAssertEqualObjects(result, pathList);
+        [resultExpectation fulfill];
       }];
   [plugin sendCallResultWithSavedPathList:pathList];
 
-  dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
+  [self waitForExpectationsWithTimeout:30 handler:nil];
+}
 
-  XCTAssertEqual(pickImageResult, pathList);
+- (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) {
+  id mockPickerViewController = OCMClassMock([PHPickerViewController class]);
+
+  id mockItemProvider = OCMClassMock([NSItemProvider class]);
+  // Does not conform to image, invalid source.
+  OCMStub([mockItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(NO);
+
+  PHPickerResult *failResult1 = OCMClassMock([PHPickerResult class]);
+  OCMStub([failResult1 itemProvider]).andReturn(mockItemProvider);
+
+  PHPickerResult *failResult2 = OCMClassMock([PHPickerResult class]);
+  OCMStub([failResult2 itemProvider]).andReturn(mockItemProvider);
+
+  FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
+
+  XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
+
+  plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^(NSArray<NSString *> *result, FlutterError *error) {
+        XCTAssertTrue(NSThread.isMainThread);
+        XCTAssertNil(result);
+        XCTAssertEqualObjects(error.code, @"invalid_source");
+        [resultExpectation fulfill];
+      }];
+
+  [plugin picker:mockPickerViewController didFinishPicking:@[ failResult1, failResult2 ]];
+
+  [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
+- (void)testSendsImageInvalidErrorWhenOneFails API_AVAILABLE(ios(14)) {
+  id mockPickerViewController = OCMClassMock([PHPickerViewController class]);
+  NSError *loadDataError = [NSError errorWithDomain:@"PHPickerDomain" code:1234 userInfo:nil];
+
+  id mockFailItemProvider = OCMClassMock([NSItemProvider class]);
+  OCMStub([mockFailItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(YES);
+  [[mockFailItemProvider stub]
+      loadDataRepresentationForTypeIdentifier:OCMOCK_ANY
+                            completionHandler:[OCMArg invokeBlockWithArgs:[NSNull null],
+                                                                          loadDataError, nil]];
+
+  PHPickerResult *failResult = OCMClassMock([PHPickerResult class]);
+  OCMStub([failResult itemProvider]).andReturn(mockFailItemProvider);
+
+  NSURL *tiffURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage"
+                                                            withExtension:@"tiff"];
+  NSItemProvider *tiffItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:tiffURL];
+  PHPickerResult *tiffResult = OCMClassMock([PHPickerResult class]);
+  OCMStub([tiffResult itemProvider]).andReturn(tiffItemProvider);
+
+  FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
+
+  XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
+
+  plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^(NSArray<NSString *> *result, FlutterError *error) {
+        XCTAssertTrue(NSThread.isMainThread);
+        XCTAssertNil(result);
+        XCTAssertEqualObjects(error.code, @"invalid_image");
+        [resultExpectation fulfill];
+      }];
+
+  [plugin picker:mockPickerViewController didFinishPicking:@[ failResult, tiffResult ]];
+
+  [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
+- (void)testSavesImages API_AVAILABLE(ios(14)) {
+  id mockPickerViewController = OCMClassMock([PHPickerViewController class]);
+
+  NSURL *tiffURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage"
+                                                            withExtension:@"tiff"];
+  NSItemProvider *tiffItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:tiffURL];
+  PHPickerResult *tiffResult = OCMClassMock([PHPickerResult class]);
+  OCMStub([tiffResult itemProvider]).andReturn(tiffItemProvider);
+
+  NSURL *pngURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage"
+                                                           withExtension:@"png"];
+  NSItemProvider *pngItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:pngURL];
+  PHPickerResult *pngResult = OCMClassMock([PHPickerResult class]);
+  OCMStub([pngResult itemProvider]).andReturn(pngItemProvider);
+
+  FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
+
+  XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
+
+  plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
+      initWithResult:^(NSArray<NSString *> *result, FlutterError *error) {
+        XCTAssertTrue(NSThread.isMainThread);
+        XCTAssertEqual(result.count, 2);
+        XCTAssertNil(error);
+        [resultExpectation fulfill];
+      }];
+
+  [plugin picker:mockPickerViewController didFinishPicking:@[ tiffResult, pngResult ]];
+
+  [self waitForExpectationsWithTimeout:30 handler:nil];
 }
 
 @end
diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m
index d211ea3..41398bf 100644
--- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m
+++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m
@@ -39,8 +39,7 @@
                                                                                maxWidth:nil
                                                                               maxHeight:nil
                                                                            imageQuality:nil];
-  XCTAssertNotNil(savedPathJPG);
-  XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4], @".jpg");
+  XCTAssertEqualObjects([NSURL URLWithString:savedPathJPG].pathExtension, @"jpg");
 
   NSDictionary *originalMetaDataJPG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataJPG];
   NSData *newDataJPG = [NSData dataWithContentsOfFile:savedPathJPG];
@@ -55,8 +54,7 @@
                                                                                maxWidth:nil
                                                                               maxHeight:nil
                                                                            imageQuality:nil];
-  XCTAssertNotNil(savedPathPNG);
-  XCTAssertEqualObjects([savedPathPNG substringFromIndex:savedPathPNG.length - 4], @".png");
+  XCTAssertEqualObjects([NSURL URLWithString:savedPathPNG].pathExtension, @"png");
 
   NSDictionary *originalMetaDataPNG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataPNG];
   NSData *newDataPNG = [NSData dataWithContentsOfFile:savedPathPNG];
@@ -69,8 +67,6 @@
   NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
                                                                            image:imageJPG
                                                                     imageQuality:nil];
-
-  XCTAssertNotNil(savedPathJPG);
   // should be saved as
   XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4],
                         kFLTImagePickerDefaultSuffix);
@@ -98,7 +94,7 @@
   // test gif
   NSData *dataGIF = ImagePickerTestImages.GIFTestData;
   UIImage *imageGIF = [UIImage imageWithData:dataGIF];
-  CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil);
+  CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil);
 
   size_t numberOfFrames = CGImageSourceGetCount(imageSource);
 
@@ -107,12 +103,12 @@
                                                                                maxWidth:nil
                                                                               maxHeight:nil
                                                                            imageQuality:nil];
-  XCTAssertNotNil(savedPathGIF);
-  XCTAssertEqualObjects([savedPathGIF substringFromIndex:savedPathGIF.length - 4], @".gif");
+  XCTAssertEqualObjects([NSURL URLWithString:savedPathGIF].pathExtension, @"gif");
 
   NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPathGIF];
 
-  CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil);
+  CGImageSourceRef newImageSource =
+      CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil);
 
   size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource);
 
@@ -124,7 +120,7 @@
   NSData *dataGIF = ImagePickerTestImages.GIFTestData;
   UIImage *imageGIF = [UIImage imageWithData:dataGIF];
 
-  CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil);
+  CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil);
 
   size_t numberOfFrames = CGImageSourceGetCount(imageSource);
 
@@ -139,7 +135,8 @@
   XCTAssertEqual(newImage.size.width, 3);
   XCTAssertEqual(newImage.size.height, 2);
 
-  CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil);
+  CGImageSourceRef newImageSource =
+      CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil);
 
   size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource);
 
diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m
index 5594b9d..d418354 100644
--- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m
+++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m
@@ -3,10 +3,10 @@
 // found in the LICENSE file.
 
 #import <OCMock/OCMock.h>
-#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
 
 @import image_picker_ios;
 @import image_picker_ios.Test;
+@import UniformTypeIdentifiers;
 @import XCTest;
 
 @interface PickerSaveImageToPathOperationTests : XCTestCase
@@ -113,6 +113,60 @@
   [self verifySavingImageWithPickerResult:result fullMetadata:YES];
 }
 
+- (void)testNonexistentImage API_AVAILABLE(ios(14)) {
+  NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"bogus"
+                                                             withExtension:@"png"];
+  NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
+  PHPickerResult *result = [self createPickerResultWithProvider:itemProvider];
+
+  XCTestExpectation *errorExpectation = [self expectationWithDescription:@"invalid source error"];
+  FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
+           initWithResult:result
+                maxHeight:@100
+                 maxWidth:@100
+      desiredImageQuality:@100
+             fullMetadata:YES
+           savedPathBlock:^(NSString *savedPath, FlutterError *error) {
+             XCTAssertEqualObjects(error.code, @"invalid_source");
+             [errorExpectation fulfill];
+           }];
+
+  [operation start];
+  [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
+- (void)testFailingImageLoad API_AVAILABLE(ios(14)) {
+  NSError *loadDataError = [NSError errorWithDomain:@"PHPickerDomain" code:1234 userInfo:nil];
+
+  id mockItemProvider = OCMClassMock([NSItemProvider class]);
+  OCMStub([mockItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(YES);
+  [[mockItemProvider stub]
+      loadDataRepresentationForTypeIdentifier:OCMOCK_ANY
+                            completionHandler:[OCMArg invokeBlockWithArgs:[NSNull null],
+                                                                          loadDataError, nil]];
+
+  id pickerResult = OCMClassMock([PHPickerResult class]);
+  OCMStub([pickerResult itemProvider]).andReturn(mockItemProvider);
+
+  XCTestExpectation *errorExpectation = [self expectationWithDescription:@"invalid image error"];
+
+  FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
+           initWithResult:pickerResult
+                maxHeight:@100
+                 maxWidth:@100
+      desiredImageQuality:@100
+             fullMetadata:YES
+           savedPathBlock:^(NSString *savedPath, FlutterError *error) {
+             XCTAssertEqualObjects(error.code, @"invalid_image");
+             XCTAssertEqualObjects(error.message, loadDataError.localizedDescription);
+             XCTAssertEqualObjects(error.details, @"PHPickerDomain");
+             [errorExpectation fulfill];
+           }];
+
+  [operation start];
+  [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
 - (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) {
   id photoAssetUtil = OCMClassMock([PHAsset class]);
 
@@ -120,10 +174,10 @@
                                                              withExtension:@"png"];
   NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
   PHPickerResult *result = [self createPickerResultWithProvider:itemProvider];
+  OCMReject([photoAssetUtil fetchAssetsWithLocalIdentifiers:OCMOCK_ANY options:OCMOCK_ANY]);
 
   [self verifySavingImageWithPickerResult:result fullMetadata:NO];
-  OCMVerify(times(0), [photoAssetUtil fetchAssetsWithLocalIdentifiers:[OCMArg any]
-                                                              options:[OCMArg any]]);
+  OCMVerifyAll(photoAssetUtil);
 }
 
 /**
@@ -153,6 +207,8 @@
 - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result
                              fullMetadata:(BOOL)fullMetadata API_AVAILABLE(ios(14)) {
   XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"];
+  XCTestExpectation *operationExpectation =
+      [self expectationWithDescription:@"Operation completed"];
 
   FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
            initWithResult:result
@@ -160,14 +216,17 @@
                  maxWidth:@100
       desiredImageQuality:@100
              fullMetadata:fullMetadata
-           savedPathBlock:^(NSString *savedPath) {
-             if ([[NSFileManager defaultManager] fileExistsAtPath:savedPath]) {
-               [pathExpectation fulfill];
-             }
+           savedPathBlock:^(NSString *savedPath, FlutterError *error) {
+             XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]);
+             [pathExpectation fulfill];
            }];
+  operation.completionBlock = ^{
+    [operationExpectation fulfill];
+  };
 
   [operation start];
-  [self waitForExpectations:@[ pathExpectation ] timeout:30];
+  [self waitForExpectationsWithTimeout:30 handler:nil];
+  XCTAssertTrue(operation.isFinished);
 }
 
 @end
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m
index 2d370aa..d5b823c 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m
@@ -120,7 +120,7 @@
   options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF;
 
   CGImageSourceRef imageSource =
-      CGImageSourceCreateWithData((CFDataRef)data, (CFDictionaryRef)options);
+      CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)options);
 
   size_t numberOfFrames = CGImageSourceGetCount(imageSource);
   NSMutableArray<UIImage *> *images = [NSMutableArray arrayWithCapacity:numberOfFrames];
@@ -128,7 +128,7 @@
   NSTimeInterval interval = 0.0;
   for (size_t index = 0; index < numberOfFrames; index++) {
     CGImageRef imageRef =
-        CGImageSourceCreateImageAtIndex(imageSource, index, (CFDictionaryRef)options);
+        CGImageSourceCreateImageAtIndex(imageSource, index, (__bridge CFDictionaryRef)options);
 
     NSDictionary *properties = (NSDictionary *)CFBridgingRelease(
         CGImageSourceCopyPropertiesAtIndex(imageSource, index, NULL));
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m
index 45bcaa7..1954625 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m
@@ -42,7 +42,7 @@
 }
 
 + (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData {
-  CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
+  CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
   NSDictionary *metadata =
       (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(source, 0, NULL));
   CFRelease(source);
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m
index 37a1a98..fef94ad 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m
@@ -103,7 +103,7 @@
                             gifInfo:(GIFInfo *)gifInfo
                                path:(NSString *)path {
   CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
-      (CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL);
+      (__bridge CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL);
 
   NSDictionary *frameProperties = @{
     (__bridge NSString *)kCGImagePropertyGIFDictionary : @{
@@ -120,11 +120,12 @@
 
   gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount] = @0;
 
-  CGImageDestinationSetProperties(destination, (CFDictionaryRef)gifMetaProperties);
+  CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)gifMetaProperties);
 
   for (NSInteger index = 0; index < gifInfo.images.count; index++) {
     UIImage *image = (UIImage *)[gifInfo.images objectAtIndex:index];
-    CGImageDestinationAddImage(destination, image.CGImage, (CFDictionaryRef)frameProperties);
+    CGImageDestinationAddImage(destination, image.CGImage,
+                               (__bridge CFDictionaryRef)frameProperties);
   }
 
   CGImageDestinationFinalize(destination);
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h
index c88db0b..626e2ba 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h
@@ -5,9 +5,9 @@
 #import <Flutter/Flutter.h>
 #import <PhotosUI/PhotosUI.h>
 
+NS_ASSUME_NONNULL_BEGIN
+
 @interface FLTImagePickerPlugin : NSObject <FlutterPlugin>
-
-// For testing only.
-- (UIViewController *)viewControllerWithWindow:(UIWindow *)window;
-
 @end
+
+NS_ASSUME_NONNULL_END
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 27b06ba..68230ed 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
@@ -29,10 +29,7 @@
 
 #pragma mark -
 
-@interface FLTImagePickerPlugin () <UINavigationControllerDelegate,
-                                    UIImagePickerControllerDelegate,
-                                    PHPickerViewControllerDelegate,
-                                    UIAdaptivePresentationControllerDelegate>
+@interface FLTImagePickerPlugin ()
 
 /**
  * The PHPickerViewController instance used to pick multiple
@@ -478,52 +475,55 @@
     [self sendCallResultWithSavedPathList:nil];
     return;
   }
-  dispatch_queue_t backgroundQueue =
-      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
-  dispatch_async(backgroundQueue, ^{
-    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];
+  __block NSOperationQueue *saveQueue = [[NSOperationQueue alloc] init];
+  saveQueue.name = @"Flutter Save Image Queue";
+  saveQueue.qualityOfService = NSQualityOfServiceUserInitiated;
 
-    for (int i = 0; i < results.count; i++) {
-      PHPickerResult *result = results[i];
-      FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
-               initWithResult:result
-                    maxHeight:maxHeight
-                     maxWidth:maxWidth
-          desiredImageQuality:desiredImageQuality
-                 fullMetadata:self.callContext.requestFullMetadata
-               savedPathBlock:^(NSString *savedPath) {
-                 pathList[i] = savedPath;
-               }];
-      [operationQueue addOperation:operation];
+  FLTImagePickerMethodCallContext *currentCallContext = self.callContext;
+  NSNumber *maxWidth = currentCallContext.maxSize.width;
+  NSNumber *maxHeight = currentCallContext.maxSize.height;
+  NSNumber *imageQuality = currentCallContext.imageQuality;
+  NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality];
+  BOOL requestFullMetadata = currentCallContext.requestFullMetadata;
+  NSMutableArray *pathList = [[NSMutableArray alloc] initWithCapacity:results.count];
+  __block FlutterError *saveError = nil;
+  __weak typeof(self) weakSelf = self;
+  // This operation will be executed on the main queue after
+  // all selected files have been saved.
+  NSBlockOperation *sendListOperation = [NSBlockOperation blockOperationWithBlock:^{
+    if (saveError != nil) {
+      [weakSelf sendCallResultWithError:saveError];
+    } else {
+      [weakSelf sendCallResultWithSavedPathList:pathList];
     }
-    [operationQueue waitUntilAllOperationsAreFinished];
-    dispatch_async(dispatch_get_main_queue(), ^{
-      [self sendCallResultWithSavedPathList:pathList];
-    });
-  });
-}
+    // Retain queue until here.
+    saveQueue = nil;
+  }];
 
-#pragma mark -
+  [results enumerateObjectsUsingBlock:^(PHPickerResult *result, NSUInteger index, BOOL *stop) {
+    // NSNull means it hasn't saved yet.
+    [pathList addObject:[NSNull null]];
+    FLTPHPickerSaveImageToPathOperation *saveOperation =
+        [[FLTPHPickerSaveImageToPathOperation alloc]
+                 initWithResult:result
+                      maxHeight:maxHeight
+                       maxWidth:maxWidth
+            desiredImageQuality:desiredImageQuality
+                   fullMetadata:requestFullMetadata
+                 savedPathBlock:^(NSString *savedPath, FlutterError *error) {
+                   if (savedPath != nil) {
+                     pathList[index] = savedPath;
+                   } else {
+                     saveError = error;
+                   }
+                 }];
+    [sendListOperation addDependency:saveOperation];
+    [saveQueue addOperation:saveOperation];
+  }];
 
-/**
- * Creates an NSMutableArray of a certain size filled with NSNull objects.
- *
- * The difference with initWithCapacity is that initWithCapacity still gives an empty array making
- * it impossible to add objects on an index larger than the size.
- *
- * @param size The length of the required array
- * @return NSMutableArray An array of a specified size
- */
-- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size {
-  NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size];
-  for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++)
-    ;
-  return mutableArray;
+  // Schedule the final Flutter callback on the main queue
+  // to be run after all images have been saved.
+  [NSOperationQueue.mainQueue addOperation:sendListOperation];
 }
 
 #pragma mark - UIImagePickerControllerDelegate
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 d73a54d..f849211 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
@@ -54,13 +54,19 @@
 #pragma mark -
 
 /** Methods exposed for unit testing. */
-@interface FLTImagePickerPlugin () <FLTImagePickerApi>
+@interface FLTImagePickerPlugin () <FLTImagePickerApi,
+                                    UINavigationControllerDelegate,
+                                    UIImagePickerControllerDelegate,
+                                    PHPickerViewControllerDelegate,
+                                    UIAdaptivePresentationControllerDelegate>
 
 /**
  * The context of the Flutter method call that is currently being handled, if any.
  */
 @property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext;
 
+- (UIViewController *)viewControllerWithWindow:(nullable UIWindow *)window;
+
 /**
  * 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.
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h
index 8e097072..00c1f1d 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h
@@ -9,6 +9,11 @@
 #import "FLTImagePickerMetaDataUtil.h"
 #import "FLTImagePickerPhotoAssetUtil.h"
 
+NS_ASSUME_NONNULL_BEGIN
+
+/// Returns either the saved path, or an error. Both cannot be set.
+typedef void (^FLTGetSavedPath)(NSString *_Nullable savedPath, FlutterError *_Nullable error);
+
 /*!
  @class FLTPHPickerSaveImageToPathOperation
 
@@ -27,6 +32,8 @@
                       maxWidth:(NSNumber *)maxWidth
            desiredImageQuality:(NSNumber *)desiredImageQuality
                   fullMetadata:(BOOL)fullMetadata
-                savedPathBlock:(void (^)(NSString *))savedPathBlock API_AVAILABLE(ios(14));
+                savedPathBlock:(FLTGetSavedPath)savedPathBlock API_AVAILABLE(ios(14));
 
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
index 16c2050..9a4ae2f 100644
--- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
+++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#import <Flutter/Flutter.h>
 #import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
 
 #import "FLTPHPickerSaveImageToPathOperation.h"
@@ -19,12 +20,10 @@
 
 @end
 
-typedef void (^GetSavedPath)(NSString *);
-
 @implementation FLTPHPickerSaveImageToPathOperation {
   BOOL executing;
   BOOL finished;
-  GetSavedPath getSavedPath;
+  FLTGetSavedPath getSavedPath;
 }
 
 - (instancetype)initWithResult:(PHPickerResult *)result
@@ -32,7 +31,7 @@
                       maxWidth:(NSNumber *)maxWidth
            desiredImageQuality:(NSNumber *)desiredImageQuality
                   fullMetadata:(BOOL)fullMetadata
-                savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) {
+                savedPathBlock:(FLTGetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) {
   if (self = [super init]) {
     if (result) {
       self.result = result;
@@ -76,10 +75,10 @@
   [self didChangeValueForKey:@"isExecuting"];
 }
 
-- (void)completeOperationWithPath:(NSString *)savedPath {
+- (void)completeOperationWithPath:(NSString *)savedPath error:(FlutterError *)error {
+  getSavedPath(savedPath, error);
   [self setExecuting:NO];
   [self setFinished:YES];
-  getSavedPath(savedPath);
 }
 
 - (void)start {
@@ -102,10 +101,18 @@
                                     UIImage *image = [[UIImage alloc] initWithData:data];
                                     [self processImage:image];
                                   } else {
-                                    os_log_error(OS_LOG_DEFAULT, "Could not process image: %@",
-                                                 error);
+                                    FlutterError *flutterError =
+                                        [FlutterError errorWithCode:@"invalid_image"
+                                                            message:error.localizedDescription
+                                                            details:error.domain];
+                                    [self completeOperationWithPath:nil error:flutterError];
                                   }
                                 }];
+    } else {
+      FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source"
+                                                       message:@"Invalid image source."
+                                                       details:nil];
+      [self completeOperationWithPath:nil error:flutterError];
     }
   } else {
     [self setFinished:YES];
@@ -139,7 +146,7 @@
                                     maxWidth:self.maxWidth
                                    maxHeight:self.maxHeight
                                 imageQuality:self.desiredImageQuality];
-          [self completeOperationWithPath:savedPath];
+          [self completeOperationWithPath:savedPath error:nil];
         };
     if (@available(iOS 13.0, *)) {
       [[PHImageManager defaultManager]
@@ -169,7 +176,7 @@
         [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
                                                         image:localImage
                                                  imageQuality:self.desiredImageQuality];
-    [self completeOperationWithPath:savedPath];
+    [self completeOperationWithPath:savedPath error:nil];
   }
 }
 
diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml
index e1b3891..44c00d7 100755
--- a/packages/image_picker/image_picker_ios/pubspec.yaml
+++ b/packages/image_picker/image_picker_ios/pubspec.yaml
@@ -2,7 +2,7 @@
 description: iOS implementation of the image_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.6+2
+version: 0.8.6+3
 
 environment:
   sdk: ">=2.14.0 <3.0.0"