-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[image_picker_ios] Pass through error message from image saving #6858
Conversation
c9b05ad
to
4ec89b8
Compare
@interface FLTImagePickerPlugin : NSObject <FlutterPlugin> | ||
|
||
// For testing only. | ||
- (UIViewController *)viewControllerWithWindow:(UIWindow *)window; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to existing FLTImagePickerPlugin_Test.h
@@ -29,10 +29,7 @@ - (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { | |||
|
|||
#pragma mark - | |||
|
|||
@interface FLTImagePickerPlugin () <UINavigationControllerDelegate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also moved to FLTImagePickerPlugin_Test.h
so the tests can directly call the PHPickerViewControllerDelegate
methods.
@@ -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 = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the fix--when the image fails to load, send back an error.
} | ||
}]; | ||
} else { | ||
FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And if it's not an image, also send back an error.
[sendListOperation addDependency:saveOperation]; | ||
[backgroundQueue addOperation:saveOperation]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use NSOperationQueue
so the main thread callback operation can be scheduled ASAP, but won't run until all the dependent save operations have completed.
4ec89b8
to
9c30e3b
Compare
9c30e3b
to
bf5fff6
Compare
dispatch_async(dispatch_get_main_queue(), ^{ | ||
__block NSOperationQueue *saveQueue = [[NSOperationQueue alloc] init]; | ||
saveQueue.name = @"Flutter Save Image Queue"; | ||
saveQueue.qualityOfService = NSQualityOfServiceUserInitiated; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This queue should not be background QoS, that's for app maintenance like clearing caches and can take awhile to schedule, or sometimes not scheduled at all until the OS is idle. But this image save is in reaction to the user tapping on image, who is waiting for the app to continue.
https://developer.apple.com/documentation/dispatch/dispatchqos/qosclass
// NSNull means it hasn't saved yet. | ||
pathList[index] = [NSNull null]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't really needed anymore, either all the images will save and this will be densely populated, or it will not be returned at all and only the error will be returned.
However if flutter/flutter#95268 is implemented then this should return all the saved images, and for the failing ones some object with the error attached.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -269,37 +271,130 @@ - (void)testViewController { | |||
- (void)testPluginMultiImagePathHasNullItem { | |||
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; | |||
|
|||
dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
@@ -120,7 +120,7 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data | |||
options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF; | |||
|
|||
CGImageSourceRef imageSource = | |||
CGImageSourceCreateWithData((CFDataRef)data, (CFDictionaryRef)options); | |||
CGImageSourceCreateWithData((__bridge CFDataRef)data, (CFDictionaryRef)options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(__bridge CFDictionaryRef)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah missed that.
NSNumber *imageQuality = currentCallContext.imageQuality; | ||
NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; | ||
BOOL requestFullMetadata = currentCallContext.requestFullMetadata; | ||
NSMutableArray *pathList = [[NSMutableArray alloc] initWithCapacity:results.count]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to populate the array like before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment below: https://github.com/flutter/plugins/pull/6858/files#r1051290080
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, maybe im missing something. I was saying if pathList
is not populated (i.e. size=0, capacity=count), then will pathList[0] = foo
throw index out of bounds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, good catch. I'm going to make this a NSPointerArray
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually just populating the array with NSNull
as was done before is probably safer. A NSPointerArray
won't have the out-of-bounds problem, since it "populates" up to count
with nil
, and will compact those out during allObjects
. So if there was a bug where all indices weren't set to NSNull
then we would get wacky index values and the array length would be incorrect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes what I have here is correct, the first thing it's doing is populating the array with NSNull
, in index order:
NSMutableArray *pathList = [[NSMutableArray alloc] initWithCapacity:results.count];
...
[results enumerateObjectsUsingBlock:^(PHPickerResult *result, NSUInteger index, BOOL *stop) {
// NSNull means it hasn't saved yet.
pathList[index] = [NSNull null];
...
This is exactly the same as the previous logic, it just replaced a for loop with fast enumeration over the results.
NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size];
for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++)
;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, the previous logic is using addObject
, which increases the size though. iirc I think pathList[index] = [NSNull null]
will simply crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just did an experiment. Very interesting/strange behavior of NSMutableArray
:
Experiment 1: This works:
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:100];
arr[0] = @"foo0";
arr[1] = @"foo1";
arr[2] = @"bar2";
NSLog(@"print out: %@", arr);
Experiment 2: This crashes:
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:100];
arr[0] = @"foo0";
arr[2] = @"bar2";
NSLog(@"print out: %@", arr);
Experiment 3: This also crashes:
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:100];
arr[1] = @"foo1";
arr[2] = @"bar2";
NSLog(@"print out: %@", arr);
Looks like as long as you add from index 0, one by one, NSMutableArray will just auto increment the size one by one. I wasn't able to find this behavior documented anywhere. However, the doc of setObject API explicitly says it should crash:
If index is beyond the end of the array (that is, if index is greater than or equal to the value returned by count), an NSRangeException is raised.
The above contradicts with actual behavior of Experiment 1, so it feels like a bug in NSMutableArray
. I recommend just do addObject
instead of setObject
API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is doing experiment 1 because enumerateObjectsUsingBlock:
starts at 0 and goes through the whole array (unless *stop = YES
is set).
https://developer.apple.com/documentation/foundation/nsarray/1415846-enumerateobjectsusingblock
Executes a given block using each object in the array, starting with the first object and continuing through the array to the last object.
I can swap to addObject:
though since it is incrementing by one every time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can swap to addObject: though since it is incrementing by one every time.
Yep, I think experiment 1 is a bug in NSMutableArray
that's supposed to crash, since it contradicts with the doc.
|
||
// This operation will be executed on the main queue after | ||
// all selected files have been saved. | ||
NSBlockOperation *sendListOperation = [NSBlockOperation blockOperationWithBlock:^{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weak self
// This operation will be executed on the main queue after | ||
// all selected files have been saved. | ||
NSBlockOperation *sendListOperation = [NSBlockOperation blockOperationWithBlock:^{ | ||
if (saveError != nil) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
may worth commenting that "No race condition here since sendListOperation
is only called after all saveOperation
s are complete"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It says a few lines above this:
// This operation will be executed on the main queue after
// all selected files have been saved.
saveError = error; | ||
} | ||
}]; | ||
[sendListOperation addDependency:saveOperation]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice API. Yeah it's weird to mix GCD and NSOperationQueue - in pure GCD I would use DispatchGroup
for this.
* 697c8b3ee [image_picker_ios] Pass through error message from image saving (flutter/plugins#6858) * 72f810851 [local_auth] Bump `intl` from ^0.17.0 to ">=0.17.0 <0.19.0" (flutter/plugins#6848) * acbe9b452 [gh_actions]: Bump github/codeql-action from 2.1.35 to 2.1.37 (flutter/plugins#6860) * 3d8b73bf0 [camera] Remove deprecated Optional type (flutter/plugins#6870) * c5220efae [in_app_purchase] Add support for macOS (flutter/plugins#6519) * 54fc2066d Roll Flutter from 028c6e2 to dbc9306 (11 revisions) (flutter/plugins#6849)
…#117456) * 697c8b3ee [image_picker_ios] Pass through error message from image saving (flutter/plugins#6858) * 72f810851 [local_auth] Bump `intl` from ^0.17.0 to ">=0.17.0 <0.19.0" (flutter/plugins#6848) * acbe9b452 [gh_actions]: Bump github/codeql-action from 2.1.35 to 2.1.37 (flutter/plugins#6860) * 3d8b73bf0 [camera] Remove deprecated Optional type (flutter/plugins#6870) * c5220efae [in_app_purchase] Add support for macOS (flutter/plugins#6519) * 54fc2066d Roll Flutter from 028c6e2 to dbc9306 (11 revisions) (flutter/plugins#6849)
…ter#6858) * [image_picker_ios] Pass through error message from image saving * Review edits * Format * addObject
Previously, if there was an error loading the image, the result callback would never be called, and the
callContext
would not be nil'd out. When the next request happened, the user would get a confusingmultiple_request
error because the last state wasn't cleared out.plugins/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
Lines 273 to 277 in 2cbb314
Now, when there is an error loading an image, pipe an error back to the flutter result instead of silently failing and never calling the result callback. This also
nil
s out thecallContext
so the next request has a chance of succeeding.plugins/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m
Lines 683 to 684 in 2cbb314
As before, save all the picked images on a background thread, and when complete call the callback with either all the saved images, or with the image error. Use the
NSOperationQueue
API instead of GCD to take advantage of dependency operations (the final main callback is dependent on the image save operations completing).If any of the picked images fail to load, return an error back to the dart side. Ideally there would be a mapping between successfully picked images and the images that failed to load, but I believe that would require an API change like flutter/flutter#95268.
Also
__bridge
cleanupFixes flutter/flutter#98569
Fixes flutter/flutter#82602
Will make flutter/flutter#49780 actually show the underlying error.
Pre-launch Checklist
dart format
.)[shared_preferences]
pubspec.yaml
with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.md
to add a description of the change, following repository CHANGELOG style.///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.