Skip to content

Commit 733869c

Browse files
authored
[image_picker] Removes use of PHAsset on IOS 14+ (#8190)
Original PR #8020 Fixes issue flutter/flutter#90373 Currently on IOS 14+, images are picked using `PHPickerViewController` which does not need photo permissions and also gets the full image metadata regardless of `requestFullMetadata`. However, it currently retrieves the metadata using `PHAsset` which does require permission and causes the gallery opening twice issue. Another issue is that an error is thrown when permission is denied even if none are required.
1 parent 4dd8ea3 commit 733869c

File tree

9 files changed

+14
-159
lines changed

9 files changed

+14
-159
lines changed

packages/image_picker/image_picker_ios/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 0.8.12+2
22

3+
* Removes the need for user permissions to pick an image on iOS 14+.
34
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
45

56
## 0.8.12+1

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

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -495,11 +495,11 @@ - (void)testSavesImages API_AVAILABLE(ios(14)) {
495495
[self waitForExpectationsWithTimeout:30 handler:nil];
496496
}
497497

498-
- (void)testPickImageRequestAuthorization API_AVAILABLE(ios(14)) {
498+
- (void)testPickImageDoesntRequestAuthorization API_AVAILABLE(ios(14)) {
499499
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
500500
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
501501
.andReturn(PHAuthorizationStatusNotDetermined);
502-
OCMExpect([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
502+
OCMReject([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
503503
handler:OCMOCK_ANY]);
504504

505505
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
@@ -514,29 +514,6 @@ - (void)testPickImageRequestAuthorization API_AVAILABLE(ios(14)) {
514514
OCMVerifyAll(mockPhotoLibrary);
515515
}
516516

517-
- (void)testPickImageAuthorizationDenied API_AVAILABLE(ios(14)) {
518-
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
519-
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
520-
.andReturn(PHAuthorizationStatusDenied);
521-
522-
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
523-
524-
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
525-
526-
[plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery
527-
camera:FLTSourceCameraFront]
528-
maxSize:[[FLTMaxSize alloc] init]
529-
quality:nil
530-
fullMetadata:YES
531-
completion:^(NSString *result, FlutterError *error) {
532-
XCTAssertNil(result);
533-
XCTAssertEqualObjects(error.code, @"photo_access_denied");
534-
XCTAssertEqualObjects(error.message, @"The user did not allow photo access.");
535-
[resultExpectation fulfill];
536-
}];
537-
[self waitForExpectationsWithTimeout:30 handler:nil];
538-
}
539-
540517
- (void)testPickMultiImageDuplicateCallCancels API_AVAILABLE(ios(14)) {
541518
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
542519
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ - (void)getAssetFromImagePickerInfoShouldReturnNilIfNotAvailable {
2020
XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:mockData]);
2121
}
2222

23-
- (void)testGetAssetFromPHPickerResultShouldReturnNilIfNotAvailable API_AVAILABLE(ios(14)) {
24-
if (@available(iOS 14, *)) {
25-
PHPickerResult *mockData;
26-
[mockData.itemProvider
27-
loadObjectOfClass:[UIImage class]
28-
completionHandler:^(__kindof id<NSItemProviderReading> _Nullable image,
29-
NSError *_Nullable error) {
30-
XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:mockData]);
31-
}];
32-
}
33-
}
34-
3523
- (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndMetaData {
3624
// test jpg
3725
NSData *dataJPG = ImagePickerTestImages.JPGTestData;

packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ - (void)testSelectingFromGallery API_AVAILABLE(ios(14)) {
104104
}
105105
[pickButton tap];
106106

107-
[self handlePermissionInterruption];
108-
109107
// Find an image and tap on it.
110108
NSPredicate *imagePredicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH 'Photo, '"];
111109
XCUIElementQuery *imageQuery = [self.app.images matchingPredicate:imagePredicate];
@@ -119,25 +117,6 @@ - (void)testSelectingFromGallery API_AVAILABLE(ios(14)) {
119117

120118
[aImage tap];
121119

122-
// Find and tap on the `Done` button.
123-
XCUIElement *doneButton = self.app.buttons[@"Done"].firstMatch;
124-
if (![doneButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) {
125-
os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription);
126-
XCTSkip(@"Permissions popup could not fired so the test is skipped...");
127-
}
128-
[doneButton tap];
129-
130-
// Find an image and tap on it to have access to selected photos.
131-
aImage = imageQuery.firstMatch;
132-
133-
os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription);
134-
if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) {
135-
os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription);
136-
XCTFail(@"Failed due to not able to find an image with %@ seconds",
137-
@(kLimitedElementWaitingTime));
138-
}
139-
[aImage tap];
140-
141120
// Find the picked image.
142121
XCUIElement *pickedImage = self.app.images[@"image_picker_example_picked_image"].firstMatch;
143122
if (![pickedImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) {

packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPhotoAssetUtil.m

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info {
1414
return info[UIImagePickerControllerPHAsset];
1515
}
1616

17-
+ (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
18-
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[ result.assetIdentifier ]
19-
options:nil];
20-
return fetchResult.firstObject;
21-
}
22-
2317
+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL {
2418
if (![[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) {
2519
return nil;

packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPlugin.m

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,7 @@ - (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)con
113113
pickerViewController.presentationController.delegate = self;
114114
self.callContext = context;
115115

116-
if (context.requestFullMetadata) {
117-
[self checkPhotoAuthorizationWithPHPicker:pickerViewController];
118-
} else {
119-
[self showPhotoLibraryWithPHPicker:pickerViewController];
120-
}
116+
[self showPhotoLibraryWithPHPicker:pickerViewController];
121117
}
122118

123119
- (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source
@@ -390,40 +386,6 @@ - (void)checkPhotoAuthorizationWithImagePicker:(UIImagePickerController *)imageP
390386
}
391387
}
392388

393-
- (void)checkPhotoAuthorizationWithPHPicker:(PHPickerViewController *)pickerViewController
394-
API_AVAILABLE(ios(14)) {
395-
PHAccessLevel requestedAccessLevel = PHAccessLevelReadWrite;
396-
PHAuthorizationStatus status =
397-
[PHPhotoLibrary authorizationStatusForAccessLevel:requestedAccessLevel];
398-
switch (status) {
399-
case PHAuthorizationStatusNotDetermined: {
400-
[PHPhotoLibrary
401-
requestAuthorizationForAccessLevel:requestedAccessLevel
402-
handler:^(PHAuthorizationStatus status) {
403-
dispatch_async(dispatch_get_main_queue(), ^{
404-
if (status == PHAuthorizationStatusAuthorized) {
405-
[self showPhotoLibraryWithPHPicker:pickerViewController];
406-
} else if (status == PHAuthorizationStatusLimited) {
407-
[self showPhotoLibraryWithPHPicker:pickerViewController];
408-
} else {
409-
[self errorNoPhotoAccess:status];
410-
}
411-
});
412-
}];
413-
break;
414-
}
415-
case PHAuthorizationStatusAuthorized:
416-
case PHAuthorizationStatusLimited:
417-
[self showPhotoLibraryWithPHPicker:pickerViewController];
418-
break;
419-
case PHAuthorizationStatusDenied:
420-
case PHAuthorizationStatusRestricted:
421-
default:
422-
[self errorNoPhotoAccess:status];
423-
break;
424-
}
425-
}
426-
427389
- (void)errorNoCameraAccess:(AVAuthorizationStatus)status {
428390
switch (status) {
429391
case AVAuthorizationStatusRestricted:

packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/FLTPHPickerSaveImageToPathOperation.m

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -128,64 +128,20 @@ - (void)start {
128128
- (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) {
129129
UIImage *localImage = [[UIImage alloc] initWithData:pickerImageData];
130130

131-
PHAsset *originalAsset;
132-
// Only if requested, fetch the full "PHAsset" metadata, which requires "Photo Library Usage"
133-
// permissions.
134-
if (self.requestFullMetadata) {
135-
originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result];
136-
}
137-
138131
if (self.maxWidth != nil || self.maxHeight != nil) {
139132
localImage = [FLTImagePickerImageUtil scaledImage:localImage
140133
maxWidth:self.maxWidth
141134
maxHeight:self.maxHeight
142135
isMetadataAvailable:YES];
143136
}
144-
if (originalAsset) {
145-
void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) =
146-
^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, NSDictionary *_Nullable info) {
147-
// maxWidth and maxHeight are used only for GIF images.
148-
NSString *savedPath = [FLTImagePickerPhotoAssetUtil
149-
saveImageWithOriginalImageData:imageData
150-
image:localImage
151-
maxWidth:self.maxWidth
152-
maxHeight:self.maxHeight
153-
imageQuality:self.desiredImageQuality];
154-
[self completeOperationWithPath:savedPath error:nil];
155-
};
156-
if (@available(iOS 13.0, *)) {
157-
[[PHImageManager defaultManager]
158-
requestImageDataAndOrientationForAsset:originalAsset
159-
options:nil
160-
resultHandler:^(NSData *_Nullable imageData,
161-
NSString *_Nullable dataUTI,
162-
CGImagePropertyOrientation orientation,
163-
NSDictionary *_Nullable info) {
164-
resultHandler(imageData, dataUTI, info);
165-
}];
166-
} else {
167-
#pragma clang diagnostic push
168-
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
169-
[[PHImageManager defaultManager]
170-
requestImageDataForAsset:originalAsset
171-
options:nil
172-
resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI,
173-
UIImageOrientation orientation, NSDictionary *_Nullable info) {
174-
resultHandler(imageData, dataUTI, info);
175-
}];
176-
#pragma clang diagnostic pop
177-
}
178-
} else {
179-
// Image picked without an original asset (e.g. User pick image without permission)
180-
// maxWidth and maxHeight are used only for GIF images.
181-
NSString *savedPath =
182-
[FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData
183-
image:localImage
184-
maxWidth:self.maxWidth
185-
maxHeight:self.maxHeight
186-
imageQuality:self.desiredImageQuality];
187-
[self completeOperationWithPath:savedPath error:nil];
188-
}
137+
// maxWidth and maxHeight are used only for GIF images.
138+
NSString *savedPath =
139+
[FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData
140+
image:localImage
141+
maxWidth:self.maxWidth
142+
maxHeight:self.maxHeight
143+
imageQuality:self.desiredImageQuality];
144+
[self completeOperationWithPath:savedPath error:nil];
189145
}
190146

191147
/// Processes the video.

packages/image_picker/image_picker_ios/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPhotoAssetUtil.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ NS_ASSUME_NONNULL_BEGIN
1414

1515
+ (nullable PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info;
1616

17-
+ (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14));
18-
1917
// Saves video to temporary URL. Returns nil on failure;
2018
+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL;
2119

packages/image_picker/image_picker_ios/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: image_picker_ios
22
description: iOS implementation of the image_picker plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
5-
version: 0.8.12+1
5+
version: 0.8.12+2
66

77
environment:
88
sdk: ^3.4.0

0 commit comments

Comments
 (0)