diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 6af3cb090ca6..58002a8630b4 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.7.5 + +* Fixes an issue where image rotation is wrong when Select Photos chose and image is scaled. +* Breaking Changes: + * Migrate to PHPicker for iOS 14 and higher versions to pick image from the photo library. + * Implement the limited permission to pick photo from the photo library when Select Photo is chose. + ## 0.7.4 * Update flutter_plugin_android_lifecycle dependency to 2.0.1 to fix an R8 issue diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj index f9895f0cc06f..4ea0e7449d0a 100644 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */; }; 680049262280D736006DD6AB /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; }; - 680049272280D79A006DD6AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */; }; @@ -23,6 +22,7 @@ 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; 9FC8F0EE229FB90B00C8D58F /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; }; + BE7AEE7926403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */; }; F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; F78AF3192342D9D7008449C7 /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; }; /* End PBXBuildFile section */ @@ -35,6 +35,13 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; + BE7AEE7126403C46006181AA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -80,6 +87,9 @@ 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = ""; }; 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ImageUtilTests.m; path = ../../../ios/Tests/ImageUtilTests.m; sourceTree = ""; }; A908FAEEA2A9B26D903C09C5 /* libPods-RunnerUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITestiOS14.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BE7AEE7026403C46006181AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ImagePickerTestImages.h; path = ../../../ios/Tests/ImagePickerTestImages.h; sourceTree = ""; }; F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ImagePickerTestImages.m; path = ../../../ios/Tests/ImagePickerTestImages.m; sourceTree = ""; }; @@ -90,7 +100,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F10E2DB84567A50A8721C9C7 /* libPods-RunnerUITests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -102,6 +111,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BE7AEE6926403C46006181AA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -159,6 +175,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 6801C8372555D726009DAF8D /* RunnerUITests */, + BE7AEE6D26403C46006181AA /* RunnerUITestiOS14 */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -170,6 +187,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, + BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */, ); name = Products; sourceTree = ""; @@ -198,6 +216,15 @@ name = "Supporting Files"; sourceTree = ""; }; + BE7AEE6D26403C46006181AA /* RunnerUITestiOS14 */ = { + isa = PBXGroup; + children = ( + BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */, + BE7AEE7026403C46006181AA /* Info.plist */, + ); + path = RunnerUITestiOS14; + sourceTree = ""; + }; CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -250,6 +277,24 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + BE7AEE6B26403C46006181AA /* RunnerUITestiOS14 */ = { + isa = PBXNativeTarget; + buildConfigurationList = BE7AEE7526403C46006181AA /* Build configuration list for PBXNativeTarget "RunnerUITestiOS14" */; + buildPhases = ( + BE7AEE6826403C46006181AA /* Sources */, + BE7AEE6926403C46006181AA /* Frameworks */, + BE7AEE6A26403C46006181AA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BE7AEE7226403C46006181AA /* PBXTargetDependency */, + ); + name = RunnerUITestiOS14; + productName = RunnerUITestiOS14; + productReference = BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -273,6 +318,11 @@ }; }; }; + BE7AEE6B26403C46006181AA = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -290,6 +340,7 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, 6801C8352555D726009DAF8D /* RunnerUITests */, + BE7AEE6B26403C46006181AA /* RunnerUITestiOS14 */, ); }; /* End PBXProject section */ @@ -316,6 +367,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BE7AEE6A26403C46006181AA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -413,6 +471,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BE7AEE6826403C46006181AA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BE7AEE7926403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -421,6 +487,11 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; }; + BE7AEE7226403C46006181AA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = BE7AEE7126403C46006181AA /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -642,6 +713,53 @@ }; name = Release; }; + BE7AEE7326403C46006181AA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.RunnerUITestiOS14; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + BE7AEE7426403C46006181AA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = NHAKRD9N7D; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.RunnerUITestiOS14; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -672,6 +790,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BE7AEE7526403C46006181AA /* Build configuration list for PBXNativeTarget "RunnerUITestiOS14" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BE7AEE7326403C46006181AA /* Debug */, + BE7AEE7426403C46006181AA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 641cb8ccafa4..b1f7ff22fdde 100755 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + + + +#import + +const int kLimitedElementWaitingTime = 30; + +@interface ImagePickerFromLimitedGalleryUITests : XCTestCase + +@property(nonatomic, strong) XCUIApplication* app; + +@end + +@implementation ImagePickerFromLimitedGalleryUITests + +- (void)setUp { + [super setUp]; + // Delete the app if already exists, to test permission popups + + self.continueAfterFailure = NO; + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; + __weak typeof(self) weakSelf = self; + [self addUIInterruptionMonitorWithDescription:@"Permission popups" + handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { + if (@available(iOS 14, *)) { + XCUIElement* limitedPhotoPermission = + [interruptingElement.buttons elementBoundByIndex:0]; + if (![limitedPhotoPermission + waitForExistenceWithTimeout: + kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find " + @"selectPhotos butt on with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [limitedPhotoPermission tap]; + } else { + XCUIElement* ok = interruptingElement.buttons[@"OK"]; + if (![ok waitForExistenceWithTimeout: + kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find ok button " + @"with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [ok tap]; + } + return YES; + }]; +} + +- (void)tearDown { + [super tearDown]; + [self.app terminate]; +} + +- (void)testSelectingFromGallery { + [self launchPickerAndSelect]; +} + +- (void)launchPickerAndSelect { + // Find and tap on the pick from gallery button. + NSPredicate* predicateToFindImageFromGalleryButton = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; + + XCUIElement* imageFromGalleryButton = + [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + if (![imageFromGalleryButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", + @(kLimitedElementWaitingTime)); + } + + XCTAssertTrue(imageFromGalleryButton.exists); + [imageFromGalleryButton tap]; + + // Find and tap on the `pick` button. + NSPredicate* predicateToFindPickButton = + [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; + + XCUIElement* pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + if (![pickButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTSkip(@"Pick button isn't found so the test is skipped..."); + } + + XCTAssertTrue(pickButton.exists); + [pickButton tap]; + + // There is a known bug where the permission popups interruption won't get fired until a tap + // happened in the app. We expect a permission popup so we do a tap here. + [self.app tap]; + + // Find an image and tap on it. (IOS 14 UI, images are showing directly) + XCUIElement* aImage; + if (@available(iOS 14, *)) { + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; + } else { + XCUIElement* selectedPhotosCell = [self.app.cells + elementMatchingPredicate:[NSPredicate + predicateWithFormat:@"label == %@", @"Selected Photos"]]; + if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find \"Selected Photos\" cell with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [selectedPhotosCell tap]; + aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView + identifier:@"PhotosGridView"] + .cells.firstMatch; + } + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); + if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find an image with %@ seconds", + @(kLimitedElementWaitingTime)); + } + XCTAssertTrue(aImage.exists); + [aImage tap]; + + // Find and tap on the `Done` button. + NSPredicate* predicateToFindDoneButton = + [NSPredicate predicateWithFormat:@"label == %@", @"Done"]; + + XCUIElement* doneButton = [self.app.buttons elementMatchingPredicate:predicateToFindDoneButton]; + if (![doneButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTSkip(@"Permissions popup could not fired so the test is skipped..."); + } + + XCTAssertTrue(doneButton.exists); + [doneButton tap]; + + // Find an image and tap on it to have access to selected photos. + if (@available(iOS 14, *)) { + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; + } else { + XCUIElement* selectedPhotosCell = [self.app.cells + elementMatchingPredicate:[NSPredicate + predicateWithFormat:@"label == %@", @"Selected Photos"]]; + if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find \"Selected Photos\" cell with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [selectedPhotosCell tap]; + aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView + identifier:@"PhotosGridView"] + .cells.firstMatch; + } + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); + if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find an image with %@ seconds", + @(kLimitedElementWaitingTime)); + } + XCTAssertTrue(aImage.exists); + [aImage tap]; + + // Find the picked image. + NSPredicate* predicateToFindPickedImage = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; + + XCUIElement* pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; + if (![pickedImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find pickedImage with %@ seconds", + @(kLimitedElementWaitingTime)); + } + + XCTAssertTrue(pickedImage.exists); +} + +@end diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m index a3ca1a1f8892..4b2163d00577 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -165,7 +165,7 @@ - (void)launchPickerAndPick { // Find an image and tap on it. (IOS 14 UI, images are showing directly) XCUIElement* aImage; if (@available(iOS 14, *)) { - aImage = self.app.scrollViews.firstMatch.images.firstMatch; + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; } else { XCUIElement* allPhotosCell = [self.app.cells elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"All Photos"]]; diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h index ce37ff715caf..0016765a0fe0 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h @@ -4,6 +4,7 @@ #import #import +#import #import "FLTImagePickerImageUtil.h" @@ -13,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN + (nullable PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info; ++ (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)); + // Save image with correct meta data and extention copied from the original asset. // maxWidth and maxHeight are used only for GIF images. + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m index caadc8ba4864..51fc37598491 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -23,6 +23,12 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info { return result.firstObject; } ++ (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) { + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[ result.assetIdentifier ] + options:nil]; + return fetchResult.firstObject; +} + + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h index 3b3551d53461..ffd23cd3df6a 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import @interface FLTImagePickerPlugin : NSObject diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index 9159ea4c990b..c368c424f2c4 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -7,23 +7,32 @@ #import #import #import +#import +#import #import #import "FLTImagePickerImageUtil.h" #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () @property(copy, nonatomic) FlutterResult result; +@property(copy, nonatomic) NSDictionary *arguments; + +@property(strong, nonatomic) PHPickerViewController *pickerViewController API_AVAILABLE(ios(14)); + @end static const int SOURCE_CAMERA = 0; static const int SOURCE_GALLERY = 1; +typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPickerClassType }; + @implementation FLTImagePickerPlugin { - NSDictionary *_arguments; UIImagePickerController *_imagePickerController; UIImagePickerControllerCameraDevice _device; } @@ -58,6 +67,47 @@ - (UIViewController *)viewControllerWithWindow:(UIWindow *)window { return topController; } +- (void)pickImageWithPHPicker:(bool)single API_AVAILABLE(ios(14)) { + PHPickerConfiguration *config = + [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; + if (!single) { + config.selectionLimit = 0; // Setting to zero allow us to pick unlimited photos + } + config.filter = [PHPickerFilter imagesFilter]; + + _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; + _pickerViewController.delegate = self; + + [self checkPhotoAuthorizationForAccessLevel]; +} + +- (void)pickImageWithUIImagePicker { + _imagePickerController = [[UIImagePickerController alloc] init]; + _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + _imagePickerController.delegate = self; + _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + + int imageSource = [[_arguments objectForKey:@"source"] intValue]; + + switch (imageSource) { + case SOURCE_CAMERA: { + NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue]; + _device = (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront + : UIImagePickerControllerCameraDeviceRear; + [self checkCameraAuthorization]; + break; + } + case SOURCE_GALLERY: + [self checkPhotoAuthorization]; + break; + default: + self.result([FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]); + break; + } +} + - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if (self.result) { self.result([FlutterError errorWithCode:@"multiple_request" @@ -67,32 +117,26 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } if ([@"pickImage" isEqualToString:call.method]) { - _imagePickerController = [[UIImagePickerController alloc] init]; - _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - _imagePickerController.delegate = self; - _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; - self.result = result; _arguments = call.arguments; - int imageSource = [[_arguments objectForKey:@"source"] intValue]; - switch (imageSource) { - case SOURCE_CAMERA: { - NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue]; - _device = (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; - [self checkCameraAuthorization]; - break; + if (imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker + if (@available(iOS 14, *)) { + // PHPicker is used + [self pickImageWithPHPicker:true]; + } else { + // UIImagePicker is used + [self pickImageWithUIImagePicker]; } - case SOURCE_GALLERY: - [self checkPhotoAuthorization]; - break; - default: - result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." - details:nil]); - break; + } else { + [self pickImageWithUIImagePicker]; + } + } else if ([@"pickMultiImage" isEqualToString:call.method]) { + if (@available(iOS 14, *)) { + self.result = result; + _arguments = call.arguments; + [self pickImageWithPHPicker:false]; } } else if ([@"pickVideo" isEqualToString:call.method]) { _imagePickerController = [[UIImagePickerController alloc] init]; @@ -193,18 +237,49 @@ - (void)checkPhotoAuthorization { switch (status) { case PHAuthorizationStatusNotDetermined: { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - if (status == PHAuthorizationStatusAuthorized) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self showPhotoLibrary]; - }); - } else { - [self errorNoPhotoAccess:status]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + [self showPhotoLibrary:UIImagePickerClassType]; + } else { + [self errorNoPhotoAccess:status]; + } + }); }]; break; } case PHAuthorizationStatusAuthorized: - [self showPhotoLibrary]; + [self showPhotoLibrary:UIImagePickerClassType]; + break; + case PHAuthorizationStatusDenied: + case PHAuthorizationStatusRestricted: + default: + [self errorNoPhotoAccess:status]; + break; + } +} + +- (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + switch (status) { + case PHAuthorizationStatusNotDetermined: { + [PHPhotoLibrary + requestAuthorizationForAccessLevel:PHAccessLevelReadWrite + handler:^(PHAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + [self showPhotoLibrary:PHPickerClassType]; + } else if (status == PHAuthorizationStatusLimited) { + [self showPhotoLibrary:PHPickerClassType]; + } else { + [self errorNoPhotoAccess:status]; + } + }); + }]; + break; + } + case PHAuthorizationStatusAuthorized: + case PHAuthorizationStatusLimited: + [self showPhotoLibrary:PHPickerClassType]; break; case PHAuthorizationStatusDenied: case PHAuthorizationStatusRestricted: @@ -246,12 +321,86 @@ - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { } } -- (void)showPhotoLibrary { +- (void)showPhotoLibrary:(ImagePickerClassType)imagePickerClassType { // No need to check if SourceType is available. It always is. - _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController - animated:YES - completion:nil]; + switch (imagePickerClassType) { + case PHPickerClassType: + [[self viewControllerWithWindow:nil] presentViewController:_pickerViewController + animated:YES + completion:nil]; + break; + case UIImagePickerClassType: + _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController + animated:YES + completion:nil]; + break; + } +} + +- (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { + if (![imageQuality isKindOfClass:[NSNumber class]]) { + imageQuality = @1; + } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) { + imageQuality = [NSNumber numberWithInt:1]; + } else { + imageQuality = @([imageQuality floatValue] / 100); + } + return imageQuality; +} + +- (void)picker:(PHPickerViewController *)picker + didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { + [picker dismissViewControllerAnimated:YES completion:nil]; + + NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; + NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; + NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; + NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; + + for (PHPickerResult *result in results) { + [result.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + if ([image isKindOfClass:[UIImage class]]) { + if (image != nil) { + __block UIImage *localImage = image; + dispatch_async(dispatch_get_main_queue(), ^{ + if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { + localImage = [FLTImagePickerImageUtil scaledImage:localImage + maxWidth:maxWidth + maxHeight:maxHeight]; + } + + PHAsset *originalAsset = + [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:result]; + + if (!originalAsset) { + // Image picked without an original asset (e.g. User took a photo directly) + [self saveImageWithPickerInfo:nil + image:localImage + imageQuality:desiredImageQuality]; + } else { + [[PHImageManager defaultManager] + requestImageDataForAsset:originalAsset + options:nil + resultHandler:^( + NSData *_Nullable imageData, NSString *_Nullable dataUTI, + UIImageOrientation orientation, NSDictionary *_Nullable info) { + // maxWidth and maxHeight are used only for GIF images. + [self saveImageWithOriginalImageData:imageData + image:localImage + maxWidth:maxWidth + maxHeight:maxHeight + imageQuality:desiredImageQuality]; + }]; + } + }); + } + } + }]; + } } - (void)imagePickerController:(UIImagePickerController *)picker @@ -295,18 +444,10 @@ - (void)imagePickerController:(UIImagePickerController *)picker if (image == nil) { image = [info objectForKey:UIImagePickerControllerOriginalImage]; } - NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; - - if (![imageQuality isKindOfClass:[NSNumber class]]) { - imageQuality = @1; - } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) { - imageQuality = [NSNumber numberWithInt:1]; - } else { - imageQuality = @([imageQuality floatValue] / 100); - } + NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { image = [FLTImagePickerImageUtil scaledImage:image maxWidth:maxWidth maxHeight:maxHeight]; @@ -315,20 +456,19 @@ - (void)imagePickerController:(UIImagePickerController *)picker PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; if (!originalAsset) { // Image picked without an original asset (e.g. User took a photo directly) - [self saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; + [self saveImageWithPickerInfo:info image:image imageQuality:desiredImageQuality]; } else { - __weak typeof(self) weakSelf = self; [[PHImageManager defaultManager] requestImageDataForAsset:originalAsset options:nil resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, UIImageOrientation orientation, NSDictionary *_Nullable info) { // maxWidth and maxHeight are used only for GIF images. - [weakSelf saveImageWithOriginalImageData:imageData - image:image - maxWidth:maxWidth - maxHeight:maxHeight - imageQuality:imageQuality]; + [self saveImageWithOriginalImageData:imageData + image:image + maxWidth:maxWidth + maxHeight:maxHeight + imageQuality:desiredImageQuality]; }]; } } diff --git a/packages/image_picker/image_picker/ios/Tests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker/ios/Tests/PhotoAssetUtilTests.m index 2a1ba75b0608..b81b29f73cef 100644 --- a/packages/image_picker/image_picker/ios/Tests/PhotoAssetUtilTests.m +++ b/packages/image_picker/image_picker/ios/Tests/PhotoAssetUtilTests.m @@ -17,6 +17,18 @@ - (void)getAssetFromImagePickerInfoShouldReturnNilIfNotAvailable { XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:mockData]); } +- (void)testGetAssetFromPHPickerResultShouldReturnNilIfNotAvailable API_AVAILABLE(ios(14)) { + if (@available(iOS 14, *)) { + PHPickerResult *mockData; + [mockData.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:mockData]); + }]; + } +} + - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndMetaData { // test jpg NSData *dataJPG = ImagePickerTestImages.JPGTestData; diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 73c6a6ed1824..500e270f49f6 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker -version: 0.7.4 +version: 0.7.5 flutter: plugin: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy