Skip to content

Commit 9ac9b1d

Browse files
jmagmanmauricioluz
authored andcommitted
[image_picker_ios] Pass through error message from image saving (flutter#6858)
* [image_picker_ios] Pass through error message from image saving * Review edits * Format * addObject
1 parent cfbd53f commit 9ac9b1d

13 files changed

+281
-105
lines changed

packages/image_picker/image_picker_ios/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.6+3
2+
3+
* Returns error on image load failure.
4+
15
## 0.8.6+2
26

37
* Fixes issue where selectable images of certain types (such as ProRAW images) could not be loaded.

packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
@import image_picker_ios;
88
@import image_picker_ios.Test;
9+
@import UniformTypeIdentifiers;
910
@import XCTest;
11+
1012
#import <OCMock/OCMock.h>
1113

1214
@interface MockViewController : UIViewController
@@ -269,37 +271,130 @@ - (void)testViewController {
269271
- (void)testPluginMultiImagePathHasNullItem {
270272
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
271273

272-
dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
273-
__block FlutterError *pickImageResult = nil;
274+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
274275
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
275276
initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
276-
pickImageResult = error;
277-
dispatch_semaphore_signal(resultSemaphore);
277+
XCTAssertEqualObjects(error.code, @"create_error");
278+
[resultExpectation fulfill];
278279
}];
279280
[plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]];
280281

281-
dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
282-
283-
XCTAssertEqualObjects(pickImageResult.code, @"create_error");
282+
[self waitForExpectationsWithTimeout:30 handler:nil];
284283
}
285284

286285
- (void)testPluginMultiImagePathHasItem {
287286
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
288287
NSArray *pathList = @[ @"test" ];
289288

290-
dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
291-
__block id pickImageResult = nil;
289+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
292290

293291
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
294292
initWithResult:^(NSArray<NSString *> *_Nullable result, FlutterError *_Nullable error) {
295-
pickImageResult = result;
296-
dispatch_semaphore_signal(resultSemaphore);
293+
XCTAssertEqualObjects(result, pathList);
294+
[resultExpectation fulfill];
297295
}];
298296
[plugin sendCallResultWithSavedPathList:pathList];
299297

300-
dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
298+
[self waitForExpectationsWithTimeout:30 handler:nil];
299+
}
300+
301+
- (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) {
302+
id mockPickerViewController = OCMClassMock([PHPickerViewController class]);
303+
304+
id mockItemProvider = OCMClassMock([NSItemProvider class]);
305+
// Does not conform to image, invalid source.
306+
OCMStub([mockItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(NO);
307+
308+
PHPickerResult *failResult1 = OCMClassMock([PHPickerResult class]);
309+
OCMStub([failResult1 itemProvider]).andReturn(mockItemProvider);
310+
311+
PHPickerResult *failResult2 = OCMClassMock([PHPickerResult class]);
312+
OCMStub([failResult2 itemProvider]).andReturn(mockItemProvider);
313+
314+
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
315+
316+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
317+
318+
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
319+
initWithResult:^(NSArray<NSString *> *result, FlutterError *error) {
320+
XCTAssertTrue(NSThread.isMainThread);
321+
XCTAssertNil(result);
322+
XCTAssertEqualObjects(error.code, @"invalid_source");
323+
[resultExpectation fulfill];
324+
}];
325+
326+
[plugin picker:mockPickerViewController didFinishPicking:@[ failResult1, failResult2 ]];
327+
328+
[self waitForExpectationsWithTimeout:30 handler:nil];
329+
}
330+
331+
- (void)testSendsImageInvalidErrorWhenOneFails API_AVAILABLE(ios(14)) {
332+
id mockPickerViewController = OCMClassMock([PHPickerViewController class]);
333+
NSError *loadDataError = [NSError errorWithDomain:@"PHPickerDomain" code:1234 userInfo:nil];
334+
335+
id mockFailItemProvider = OCMClassMock([NSItemProvider class]);
336+
OCMStub([mockFailItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(YES);
337+
[[mockFailItemProvider stub]
338+
loadDataRepresentationForTypeIdentifier:OCMOCK_ANY
339+
completionHandler:[OCMArg invokeBlockWithArgs:[NSNull null],
340+
loadDataError, nil]];
341+
342+
PHPickerResult *failResult = OCMClassMock([PHPickerResult class]);
343+
OCMStub([failResult itemProvider]).andReturn(mockFailItemProvider);
344+
345+
NSURL *tiffURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage"
346+
withExtension:@"tiff"];
347+
NSItemProvider *tiffItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:tiffURL];
348+
PHPickerResult *tiffResult = OCMClassMock([PHPickerResult class]);
349+
OCMStub([tiffResult itemProvider]).andReturn(tiffItemProvider);
350+
351+
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
352+
353+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
354+
355+
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
356+
initWithResult:^(NSArray<NSString *> *result, FlutterError *error) {
357+
XCTAssertTrue(NSThread.isMainThread);
358+
XCTAssertNil(result);
359+
XCTAssertEqualObjects(error.code, @"invalid_image");
360+
[resultExpectation fulfill];
361+
}];
362+
363+
[plugin picker:mockPickerViewController didFinishPicking:@[ failResult, tiffResult ]];
364+
365+
[self waitForExpectationsWithTimeout:30 handler:nil];
366+
}
367+
368+
- (void)testSavesImages API_AVAILABLE(ios(14)) {
369+
id mockPickerViewController = OCMClassMock([PHPickerViewController class]);
370+
371+
NSURL *tiffURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage"
372+
withExtension:@"tiff"];
373+
NSItemProvider *tiffItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:tiffURL];
374+
PHPickerResult *tiffResult = OCMClassMock([PHPickerResult class]);
375+
OCMStub([tiffResult itemProvider]).andReturn(tiffItemProvider);
376+
377+
NSURL *pngURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage"
378+
withExtension:@"png"];
379+
NSItemProvider *pngItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:pngURL];
380+
PHPickerResult *pngResult = OCMClassMock([PHPickerResult class]);
381+
OCMStub([pngResult itemProvider]).andReturn(pngItemProvider);
382+
383+
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
384+
385+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
386+
387+
plugin.callContext = [[FLTImagePickerMethodCallContext alloc]
388+
initWithResult:^(NSArray<NSString *> *result, FlutterError *error) {
389+
XCTAssertTrue(NSThread.isMainThread);
390+
XCTAssertEqual(result.count, 2);
391+
XCTAssertNil(error);
392+
[resultExpectation fulfill];
393+
}];
394+
395+
[plugin picker:mockPickerViewController didFinishPicking:@[ tiffResult, pngResult ]];
301396

302-
XCTAssertEqual(pickImageResult, pathList);
397+
[self waitForExpectationsWithTimeout:30 handler:nil];
303398
}
304399

305400
@end

packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM
3939
maxWidth:nil
4040
maxHeight:nil
4141
imageQuality:nil];
42-
XCTAssertNotNil(savedPathJPG);
43-
XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4], @".jpg");
42+
XCTAssertEqualObjects([NSURL URLWithString:savedPathJPG].pathExtension, @"jpg");
4443

4544
NSDictionary *originalMetaDataJPG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataJPG];
4645
NSData *newDataJPG = [NSData dataWithContentsOfFile:savedPathJPG];
@@ -55,8 +54,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM
5554
maxWidth:nil
5655
maxHeight:nil
5756
imageQuality:nil];
58-
XCTAssertNotNil(savedPathPNG);
59-
XCTAssertEqualObjects([savedPathPNG substringFromIndex:savedPathPNG.length - 4], @".png");
57+
XCTAssertEqualObjects([NSURL URLWithString:savedPathPNG].pathExtension, @"png");
6058

6159
NSDictionary *originalMetaDataPNG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataPNG];
6260
NSData *newDataPNG = [NSData dataWithContentsOfFile:savedPathPNG];
@@ -69,8 +67,6 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithDefaultExtention {
6967
NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
7068
image:imageJPG
7169
imageQuality:nil];
72-
73-
XCTAssertNotNil(savedPathJPG);
7470
// should be saved as
7571
XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4],
7672
kFLTImagePickerDefaultSuffix);
@@ -98,7 +94,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation {
9894
// test gif
9995
NSData *dataGIF = ImagePickerTestImages.GIFTestData;
10096
UIImage *imageGIF = [UIImage imageWithData:dataGIF];
101-
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil);
97+
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil);
10298

10399
size_t numberOfFrames = CGImageSourceGetCount(imageSource);
104100

@@ -107,12 +103,12 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation {
107103
maxWidth:nil
108104
maxHeight:nil
109105
imageQuality:nil];
110-
XCTAssertNotNil(savedPathGIF);
111-
XCTAssertEqualObjects([savedPathGIF substringFromIndex:savedPathGIF.length - 4], @".gif");
106+
XCTAssertEqualObjects([NSURL URLWithString:savedPathGIF].pathExtension, @"gif");
112107

113108
NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPathGIF];
114109

115-
CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil);
110+
CGImageSourceRef newImageSource =
111+
CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil);
116112

117113
size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource);
118114

@@ -124,7 +120,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation {
124120
NSData *dataGIF = ImagePickerTestImages.GIFTestData;
125121
UIImage *imageGIF = [UIImage imageWithData:dataGIF];
126122

127-
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil);
123+
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil);
128124

129125
size_t numberOfFrames = CGImageSourceGetCount(imageSource);
130126

@@ -139,7 +135,8 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation {
139135
XCTAssertEqual(newImage.size.width, 3);
140136
XCTAssertEqual(newImage.size.height, 2);
141137

142-
CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil);
138+
CGImageSourceRef newImageSource =
139+
CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil);
143140

144141
size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource);
145142

packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
// found in the LICENSE file.
44

55
#import <OCMock/OCMock.h>
6-
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
76

87
@import image_picker_ios;
98
@import image_picker_ios.Test;
9+
@import UniformTypeIdentifiers;
1010
@import XCTest;
1111

1212
@interface PickerSaveImageToPathOperationTests : XCTestCase
@@ -113,17 +113,71 @@ - (void)testSaveTIFFImage API_AVAILABLE(ios(14)) {
113113
[self verifySavingImageWithPickerResult:result fullMetadata:YES];
114114
}
115115

116+
- (void)testNonexistentImage API_AVAILABLE(ios(14)) {
117+
NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"bogus"
118+
withExtension:@"png"];
119+
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
120+
PHPickerResult *result = [self createPickerResultWithProvider:itemProvider];
121+
122+
XCTestExpectation *errorExpectation = [self expectationWithDescription:@"invalid source error"];
123+
FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
124+
initWithResult:result
125+
maxHeight:@100
126+
maxWidth:@100
127+
desiredImageQuality:@100
128+
fullMetadata:YES
129+
savedPathBlock:^(NSString *savedPath, FlutterError *error) {
130+
XCTAssertEqualObjects(error.code, @"invalid_source");
131+
[errorExpectation fulfill];
132+
}];
133+
134+
[operation start];
135+
[self waitForExpectationsWithTimeout:30 handler:nil];
136+
}
137+
138+
- (void)testFailingImageLoad API_AVAILABLE(ios(14)) {
139+
NSError *loadDataError = [NSError errorWithDomain:@"PHPickerDomain" code:1234 userInfo:nil];
140+
141+
id mockItemProvider = OCMClassMock([NSItemProvider class]);
142+
OCMStub([mockItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(YES);
143+
[[mockItemProvider stub]
144+
loadDataRepresentationForTypeIdentifier:OCMOCK_ANY
145+
completionHandler:[OCMArg invokeBlockWithArgs:[NSNull null],
146+
loadDataError, nil]];
147+
148+
id pickerResult = OCMClassMock([PHPickerResult class]);
149+
OCMStub([pickerResult itemProvider]).andReturn(mockItemProvider);
150+
151+
XCTestExpectation *errorExpectation = [self expectationWithDescription:@"invalid image error"];
152+
153+
FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
154+
initWithResult:pickerResult
155+
maxHeight:@100
156+
maxWidth:@100
157+
desiredImageQuality:@100
158+
fullMetadata:YES
159+
savedPathBlock:^(NSString *savedPath, FlutterError *error) {
160+
XCTAssertEqualObjects(error.code, @"invalid_image");
161+
XCTAssertEqualObjects(error.message, loadDataError.localizedDescription);
162+
XCTAssertEqualObjects(error.details, @"PHPickerDomain");
163+
[errorExpectation fulfill];
164+
}];
165+
166+
[operation start];
167+
[self waitForExpectationsWithTimeout:30 handler:nil];
168+
}
169+
116170
- (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) {
117171
id photoAssetUtil = OCMClassMock([PHAsset class]);
118172

119173
NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage"
120174
withExtension:@"png"];
121175
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
122176
PHPickerResult *result = [self createPickerResultWithProvider:itemProvider];
177+
OCMReject([photoAssetUtil fetchAssetsWithLocalIdentifiers:OCMOCK_ANY options:OCMOCK_ANY]);
123178

124179
[self verifySavingImageWithPickerResult:result fullMetadata:NO];
125-
OCMVerify(times(0), [photoAssetUtil fetchAssetsWithLocalIdentifiers:[OCMArg any]
126-
options:[OCMArg any]]);
180+
OCMVerifyAll(photoAssetUtil);
127181
}
128182

129183
/**
@@ -153,21 +207,26 @@ - (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvide
153207
- (void)verifySavingImageWithPickerResult:(PHPickerResult *)result
154208
fullMetadata:(BOOL)fullMetadata API_AVAILABLE(ios(14)) {
155209
XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"];
210+
XCTestExpectation *operationExpectation =
211+
[self expectationWithDescription:@"Operation completed"];
156212

157213
FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
158214
initWithResult:result
159215
maxHeight:@100
160216
maxWidth:@100
161217
desiredImageQuality:@100
162218
fullMetadata:fullMetadata
163-
savedPathBlock:^(NSString *savedPath) {
164-
if ([[NSFileManager defaultManager] fileExistsAtPath:savedPath]) {
165-
[pathExpectation fulfill];
166-
}
219+
savedPathBlock:^(NSString *savedPath, FlutterError *error) {
220+
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]);
221+
[pathExpectation fulfill];
167222
}];
223+
operation.completionBlock = ^{
224+
[operationExpectation fulfill];
225+
};
168226

169227
[operation start];
170-
[self waitForExpectations:@[ pathExpectation ] timeout:30];
228+
[self waitForExpectationsWithTimeout:30 handler:nil];
229+
XCTAssertTrue(operation.isFinished);
171230
}
172231

173232
@end

packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,15 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data
120120
options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF;
121121

122122
CGImageSourceRef imageSource =
123-
CGImageSourceCreateWithData((CFDataRef)data, (CFDictionaryRef)options);
123+
CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)options);
124124

125125
size_t numberOfFrames = CGImageSourceGetCount(imageSource);
126126
NSMutableArray<UIImage *> *images = [NSMutableArray arrayWithCapacity:numberOfFrames];
127127

128128
NSTimeInterval interval = 0.0;
129129
for (size_t index = 0; index < numberOfFrames; index++) {
130130
CGImageRef imageRef =
131-
CGImageSourceCreateImageAtIndex(imageSource, index, (CFDictionaryRef)options);
131+
CGImageSourceCreateImageAtIndex(imageSource, index, (__bridge CFDictionaryRef)options);
132132

133133
NSDictionary *properties = (NSDictionary *)CFBridgingRelease(
134134
CGImageSourceCopyPropertiesAtIndex(imageSource, index, NULL));

packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ + (NSString *)imageTypeSuffixFromType:(FLTImagePickerMIMEType)type {
4242
}
4343

4444
+ (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData {
45-
CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
45+
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
4646
NSDictionary *metadata =
4747
(NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(source, 0, NULL));
4848
CFRelease(source);

0 commit comments

Comments
 (0)