From 697c8b3ee13113a2eeeca59e0b9972bd4b418228 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Mon, 19 Dec 2022 14:58:20 -0800 Subject: [PATCH] [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 --- .../image_picker_ios/CHANGELOG.md | 4 + .../ios/RunnerTests/ImagePickerPluginTests.m | 121 ++++++++++++++++-- .../ios/RunnerTests/PhotoAssetUtilTests.m | 21 ++- .../PickerSaveImageToPathOperationTests.m | 75 +++++++++-- .../ios/Classes/FLTImagePickerImageUtil.m | 4 +- .../ios/Classes/FLTImagePickerMetaDataUtil.m | 2 +- .../Classes/FLTImagePickerPhotoAssetUtil.m | 7 +- .../ios/Classes/FLTImagePickerPlugin.h | 8 +- .../ios/Classes/FLTImagePickerPlugin.m | 98 +++++++------- .../ios/Classes/FLTImagePickerPlugin_Test.h | 8 +- .../FLTPHPickerSaveImageToPathOperation.h | 9 +- .../FLTPHPickerSaveImageToPathOperation.m | 27 ++-- .../image_picker_ios/pubspec.yaml | 2 +- 13 files changed, 281 insertions(+), 105 deletions(-) diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index bff6dd7394f2..f51f46cac34c 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 320582b0f8a3..14491b221d29 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 @interface MockViewController : UIViewController @@ -269,37 +271,130 @@ - (void)testViewController { - (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 *_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 *_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]; +} + +- (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 *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 *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 *result, FlutterError *error) { + XCTAssertTrue(NSThread.isMainThread); + XCTAssertEqual(result.count, 2); + XCTAssertNil(error); + [resultExpectation fulfill]; + }]; + + [plugin picker:mockPickerViewController didFinishPicking:@[ tiffResult, pngResult ]]; - XCTAssertEqual(pickImageResult, pathList); + [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 d211ea3f91df..41398bf7d3e3 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 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM 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 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM 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 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithDefaultExtention { 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 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation { // 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 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation { 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 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation { 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 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation { 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 5594b9d4dc28..d418354f5cc4 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 -#import @import image_picker_ios; @import image_picker_ios.Test; +@import UniformTypeIdentifiers; @import XCTest; @interface PickerSaveImageToPathOperationTests : XCTestCase @@ -113,6 +113,60 @@ - (void)testSaveTIFFImage API_AVAILABLE(ios(14)) { [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 @@ - (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) { 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 @@ - (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvide - (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 @@ - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result 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 2d370aa2e6c8..d5b823cf90a1 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 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data 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 *images = [NSMutableArray arrayWithCapacity:numberOfFrames]; @@ -128,7 +128,7 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data 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 45bcaa7191f7..195462533544 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 @@ + (NSString *)imageTypeSuffixFromType:(FLTImagePickerMIMEType)type { } + (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 37a1a9897cd3..fef94ad30bea 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 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData 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 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData 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 c88db0bad72f..626e2ba77d67 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 #import -@interface FLTImagePickerPlugin : NSObject - -// For testing only. -- (UIViewController *)viewControllerWithWindow:(UIWindow *)window; +NS_ASSUME_NONNULL_BEGIN +@interface FLTImagePickerPlugin : NSObject @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 27b06ba994ef..68230edb8b52 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 @@ - (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { #pragma mark - -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () /** * The PHPickerViewController instance used to pick multiple @@ -478,52 +475,55 @@ - (void)picker:(PHPickerViewController *)picker [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]; - - 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]; + __block NSOperationQueue *saveQueue = [[NSOperationQueue alloc] init]; + saveQueue.name = @"Flutter Save Image Queue"; + saveQueue.qualityOfService = NSQualityOfServiceUserInitiated; + + 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]; - }); - }); -} - -#pragma mark - - -/** - * 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; + // Retain queue until here. + saveQueue = nil; + }]; + + [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]; + }]; + + // 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 d73a54d245f6..f84921160a31 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 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro #pragma mark - /** Methods exposed for unit testing. */ -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () /** * 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 8e0970725e90..00c1f1dacd6c 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 16c205012785..9a4ae2fb00fd 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 #import #import "FLTPHPickerSaveImageToPathOperation.h" @@ -19,12 +20,10 @@ @interface FLTPHPickerSaveImageToPathOperation () @end -typedef void (^GetSavedPath)(NSString *); - @implementation FLTPHPickerSaveImageToPathOperation { BOOL executing; BOOL finished; - GetSavedPath getSavedPath; + FLTGetSavedPath getSavedPath; } - (instancetype)initWithResult:(PHPickerResult *)result @@ -32,7 +31,7 @@ - (instancetype)initWithResult:(PHPickerResult *)result 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 @@ - (void)setExecuting:(BOOL)isExecuting { [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 @@ - (void)start { 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 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { 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 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { [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 e1b389161d75..44c00d7426e9 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_ios 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"