This is an automated email from the ASF dual-hosted git repository.

manuelbeck pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-plugin-camera.git


The following commit(s) were added to refs/heads/master by this push:
     new dc682b2  feat(ios): use `PHPickerViewController` for iOS 14+ (#937)
dc682b2 is described below

commit dc682b2532255252100467881e4f611644e3a6e3
Author: Manuel Beck <[email protected]>
AuthorDate: Tue Jan 13 08:33:59 2026 +0100

    feat(ios): use `PHPickerViewController` for iOS 14+ (#937)
    
    - Does not need any permissions for reading images
    - The PHPickerViewController class is an alternative to 
UIImagePickerController. PHPickerViewController improves stability and 
reliability, and includes several benefits to developers and users, such as the 
following:
    - Deferred image loading and recovery UI
    - Reliable handling of large and complex assets, like RAW and panoramic 
images
    - User-selectable assets that aren’t available for UIImagePickerController
    - Configuration of the picker to display only Live Photos
    - Availability of PHLivePhoto objects without library access
    - Stricter validations against invalid inputs
    - See documentation of PHPickerViewController: 
https://developer.apple.com/documentation/photosui/phpickerviewcontroller?language=objc
    - Added tests for PHPickerViewController in `CameraTest.m`
    
    * Documentation and formatting
    
    - Document `takePicture` and `showCameraPicker` in `CDVCamera.m`
    - A pragmas for UIImagePickerControllerDelegate methods and 
CLLocationManager methods
    - Format some long methods declarations to multi-line instead single-line 
for better readability
    - Remove unnecessry `dispatch_async(dispatch_get_main_queue() ...` in 
`takePicture` before calling `showCameraPicker`. This is already done in 
`showCameraPicker`.
    - Source out code for permission denied alert dialog when accessing the 
camera or UIImagePickerController on iOS < 14 for picking images
    
    * feat(ios): proper formatting of methods
    
    - Use linux brace style: A brace have to be on a new line for method 
declarations
    - Remove unnecessary whitespaces in method declrations
    
    * doc: readme update
    
    - Better document usage descriptions
    - `NSPhotoLibraryUsageDescription` not needed for iOS 14+ when only picking 
images
    - Improve formatting for xml, js
    - sourceType `SAVEDPHOTOALBUM` is the same as `PHOTOLIBRARY` on Android and 
iOS 14+
    - Use `PHOTOLIBRARY` as sourceType instead of `SAVEDPHOTOALBUM` in  photo 
picker example
    
    * Android: Document `SAVEDPHOTOALBUM``
    
    - Make clear that `SAVEDPHOTOALBUM` is the same like `PHOTOLIBRARY` and has 
only an effect on iOS < 14
    - Format code when creating image chooser and document the request code 
parameter
---
 README.md                                          |  90 ++--
 src/android/CameraLauncher.java                    |  15 +-
 src/ios/CDVCamera.h                                |  55 ++-
 src/ios/CDVCamera.m                                | 472 +++++++++++++++++----
 .../CDVCameraTest/CDVCameraLibTests/CameraTest.m   | 153 ++++++-
 types/index.d.ts                                   |   2 +-
 www/CameraConstants.js                             |  10 +-
 7 files changed, 637 insertions(+), 160 deletions(-)

diff --git a/README.md b/README.md
index 14ddee9..ddf0dff 100644
--- a/README.md
+++ b/README.md
@@ -30,11 +30,13 @@ the system's image library.
 
 Although the object is attached to the global scoped `navigator`, it is not 
available until after the `deviceready` event.
 
-    document.addEventListener("deviceready", onDeviceReady, false);
-    function onDeviceReady() {
-        console.log(navigator.camera);
-    }
+```js
+document.addEventListener("deviceready", onDeviceReady, false);
 
+function onDeviceReady() {
+    console.log(navigator.camera);
+}
+```
 
 ## Installation
 
@@ -65,41 +67,37 @@ In order for your changes to be accepted, you need to sign 
and submit an Apache
 
 **And don't forget to test and document your code.**
 
-### iOS Quirks
+### iOS Specifics
 
-Since iOS 10 it's mandatory to provide an usage description in the 
`info.plist` if trying to access privacy-sensitive data. When the system 
prompts the user to allow access, this usage description string will displayed 
as part of the permission dialog box, but if you didn't provide the usage 
description, the app will crash before showing the dialog. Also, Apple will 
reject apps that access private data but don't provide an usage description.
+Since iOS 10 it's mandatory to provide a usage description in the `info.plist` 
when accessing privacy-sensitive data. The required keys depend on how you use 
the plugin and which iOS versions you support:
 
-This plugins requires the following usage descriptions:
+| Key                            | Description |
+| ------------------------------ | ----------- |
+| NSCameraUsageDescription | Required whenever the camera is used (e.g. 
`Camera.PictureSourceType.CAMERA`). |
+| NSPhotoLibraryUsageDescription | Required only when your app runs on iOS 13 
or older and using as `sourceType` `Camera.PictureSourceType.PHOTOLIBRARY`. On 
iOS 14+ the plugin uses PHPicker for read-only access, which does not need this 
key. |
+| NSPhotoLibraryAddUsageDescription | Required when the plugin writes to the 
user's library (e.g. `saveToPhotoAlbum=true`). |
+| NSLocationWhenInUseUsageDescription | Required if `CameraUsesGeolocation` is 
set to `true`. |
 
-- `NSCameraUsageDescription` specifies the reason for your app to access the 
device's camera.
-- `NSPhotoLibraryUsageDescription` specifies the reason for your app to access 
the user's photo library.
-- `NSLocationWhenInUseUsageDescription` specifies the reason for your app to 
access the user's location information while your app is in use. (Set it if you 
have `CameraUsesGeolocation` preference set to `true`)
-- `NSPhotoLibraryAddUsageDescription` specifies the reason for your app to get 
write-only access to the user's photo library
+When the system prompts the user to allow access, this usage description 
string will be displayed as part of the permission dialog box. If you don't 
provide the required usage description, the app will crash before showing the 
dialog. Also, Apple will reject apps that access private data but don't provide 
a usage description.
 
 To add these entries into the `info.plist`, you can use the `edit-config` tag 
in the `config.xml` like this:
 
-```
+```xml
 <edit-config target="NSCameraUsageDescription" file="*-Info.plist" 
mode="merge">
     <string>need camera access to take pictures</string>
 </edit-config>
-```
 
-```
 <edit-config target="NSPhotoLibraryUsageDescription" file="*-Info.plist" 
mode="merge">
     <string>need photo library access to get pictures from there</string>
 </edit-config>
-```
 
-```
-<edit-config target="NSLocationWhenInUseUsageDescription" file="*-Info.plist" 
mode="merge">
-    <string>need location access to find things nearby</string>
-</edit-config>
-```
-
-```
 <edit-config target="NSPhotoLibraryAddUsageDescription" file="*-Info.plist" 
mode="merge">
     <string>need photo library access to save pictures there</string>
 </edit-config>
+
+<edit-config target="NSLocationWhenInUseUsageDescription" file="*-Info.plist" 
mode="merge">
+    <string>need location access to find things nearby</string>
+</edit-config>
 ```
 
 ---
@@ -368,9 +366,11 @@ Defines the output format of `Camera.getPicture` call.
 
 | Name | Type | Default | Description |
 | --- | --- | --- | --- |
-| PHOTOLIBRARY | <code>number</code> | <code>0</code> | Choose image from the 
device's photo library (same as SAVEDPHOTOALBUM for Android) |
+| PHOTOLIBRARY | <code>number</code> | <code>0</code> | Choose image from the 
device's photo library. |
 | CAMERA | <code>number</code> | <code>1</code> | Take picture from camera |
-| SAVEDPHOTOALBUM | <code>number</code> | <code>2</code> | Choose image only 
from the device's Camera Roll album (same as PHOTOLIBRARY for Android) |
+| SAVEDPHOTOALBUM | <code>number</code> | <code>2</code> | Same as 
`PHOTOLIBRARY`, when running on Android or iOS 14+. On iOS older than 14, an 
image can only be chosen from the device's Camera Roll album with this setting. 
|
+
+
 
 <a name="module_Camera.PopoverArrowDirection"></a>
 
@@ -643,23 +643,25 @@ function openCamera(selection) {
 
 ## Select a File from the Picture Library <a name="selectFile"></a>
 
-When selecting a file using the file picker, you also need to set the 
CameraOptions object. In this example, set the `sourceType` to 
`Camera.PictureSourceType.SAVEDPHOTOALBUM`. To open the file picker, call 
`getPicture` just as you did in the previous example, passing in the success 
and error callbacks along with CameraOptions object.
+When selecting a file using the file picker, you also need to set the 
CameraOptions object. In this example, set the `sourceType` to 
`Camera.PictureSourceType.PHOTOLIBRARY`. To open the file picker, call 
`getPicture` just as you did in the previous example, passing in the success 
and error callbacks along with CameraOptions object.
 
 ```js
 function openFilePicker(selection) {
 
-    var srcType = Camera.PictureSourceType.SAVEDPHOTOALBUM;
+    var srcType = Camera.PictureSourceType.PHOTOLIBRARY;
     var options = setOptions(srcType);
     var func = createNewFileEntry;
 
-    navigator.camera.getPicture(function cameraSuccess(imageUri) {
-
-        // Do something
-
-    }, function cameraError(error) {
-        console.debug("Unable to obtain picture: " + error, "app");
-
-    }, options);
+    navigator.camera.getPicture(
+        // success callback
+        (imageUri) => {
+            // Do something
+        },
+        // error callback
+        (error) => {
+            console.debug("Unable to obtain picture: " + error, "app");
+        },
+        options);
 }
 ```
 
@@ -670,7 +672,7 @@ Resizing a file selected with the file picker works just 
like resizing using the
 ```js
 function openFilePicker(selection) {
 
-    var srcType = Camera.PictureSourceType.SAVEDPHOTOALBUM;
+    var srcType = Camera.PictureSourceType.PHOTOLIBRARY;
     var options = setOptions(srcType);
     var func = createNewFileEntry;
 
@@ -681,14 +683,16 @@ function openFilePicker(selection) {
         options.targetWidth = 100;
     }
 
-    navigator.camera.getPicture(function cameraSuccess(imageUri) {
-
-        // Do something with image
-
-    }, function cameraError(error) {
-        console.debug("Unable to obtain picture: " + error, "app");
-
-    }, options);
+    navigator.camera.getPicture(
+        // success callback
+        (imageUri) {
+            // Do something with image
+        },
+        // error callback
+        (error) => {
+            console.debug("Unable to obtain picture: " + error, "app");
+        },
+        options);
 }
 ```
 
diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java
index f110491..8316dd4 100644
--- a/src/android/CameraLauncher.java
+++ b/src/android/CameraLauncher.java
@@ -77,9 +77,10 @@ public class CameraLauncher extends CordovaPlugin implements 
MediaScannerConnect
     private static final int DATA_URL = 0;              // Return base64 
encoded string
     private static final int FILE_URI = 1;              // Return file uri 
(content://media/external/images/media/2 for Android)
 
-    private static final int PHOTOLIBRARY = 0;          // Choose image from 
picture library (same as SAVEDPHOTOALBUM for Android)
+    private static final int PHOTOLIBRARY = 0;          // Choose image from 
picture library
     private static final int CAMERA = 1;                // Take picture from 
camera
-    private static final int SAVEDPHOTOALBUM = 2;       // Choose image from 
picture library (same as PHOTOLIBRARY for Android)
+    private static final int SAVEDPHOTOALBUM = 2;       // Same as 
PHOTOLIBRARY. This settings makes only a difference on iOS older than 14,
+                                                        // where an image can 
only be chosen from the device's Camera Roll album, with this setting.
 
     private static final int PICTURE = 0;               // allow selection of 
still pictures only. DEFAULT. Will return format specified via DestinationType
     private static final int VIDEO = 1;                 // allow selection of 
video only, ONLY RETURNS URL
@@ -423,9 +424,15 @@ public class CameraLauncher extends CordovaPlugin 
implements MediaScannerConnect
             intent.setAction(Intent.ACTION_GET_CONTENT);
             intent.addCategory(Intent.CATEGORY_OPENABLE);
         }
+
         if (this.cordova != null) {
-            this.cordova.startActivityForResult((CordovaPlugin) this, 
Intent.createChooser(intent,
-                    new String(title)), (srcType + 1) * 16 + returnType + 1);
+            this.cordova.startActivityForResult(
+                (CordovaPlugin) this,
+                Intent.createChooser(
+                    intent,
+                    new String(title)),
+                    // Requestcode
+                    (srcType + 1) * 16 + returnType + 1);
         }
     }
 
diff --git a/src/ios/CDVCamera.h b/src/ios/CDVCamera.h
index 647ffab..1bbc167 100644
--- a/src/ios/CDVCamera.h
+++ b/src/ios/CDVCamera.h
@@ -22,6 +22,26 @@
 #import <CoreLocation/CLLocationManager.h>
 #import <Cordova/CDVPlugin.h>
 
+// Since iOS 14, we can use PHPickerViewController to select images from the 
photo library
+//
+// The following condition checks if the iOS 14 SDK is available for XCode
+// which is true for XCode 12+. It does not check on runtime, if the device is 
running iOS 14+.
+// For that API_AVAILABLE(ios(14)) is used for methods declarations and 
@available(iOS 14, *) for the code.
+// The condition here makes just sure that the code can compile in XCode
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
+
+// Import UniformTypeIdentifiers.h for using UTType* things, available since 
iOS 14,
+// which replaces for e.g. kUTTypeImage with UTTypeImage, which must be used 
in the future instead
+// Currently only used for PHPickerViewController
+#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
+
+// Import PhotosUI framework for using PHPickerViewController
+// PhotosUI is already available since iOS 8, but since we need it currently
+// only for the PHPickerViewController, we import it conditionally here
+#import <PhotosUI/PhotosUI.h>
+
+#endif
+
 enum CDVDestinationType {
     DestinationTypeDataUrl = 0,
     DestinationTypeFileUri
@@ -78,38 +98,51 @@ typedef NSUInteger CDVMediaType;
 @end
 
 // ======================================================================= //
-
+// Use PHPickerViewController in iOS 14+ to select images from the photo 
library
+// PHPickerViewControllerDelegate is only available since iOS 14
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
+@interface CDVCamera : CDVPlugin <UIImagePickerControllerDelegate,
+                       UINavigationControllerDelegate,
+                       UIPopoverControllerDelegate,
+                       CLLocationManagerDelegate,
+                       PHPickerViewControllerDelegate>
+{}
+#else
 @interface CDVCamera : CDVPlugin <UIImagePickerControllerDelegate,
                        UINavigationControllerDelegate,
                        UIPopoverControllerDelegate,
                        CLLocationManagerDelegate>
 {}
+#endif
 
 @property (strong) CDVCameraPicker* pickerController;
 @property (strong) NSMutableDictionary *metadata;
 @property (strong, nonatomic) CLLocationManager *locationManager;
 @property (strong) NSData* data;
 
-/*
- * getPicture
- *
- * arguments:
- *     1: this is the javascript function that will be called with the 
results, the first parameter passed to the
- *             javascript function is the picture as a Base64 encoded string
- *  2: this is the javascript function to be called if there was an error
- * options:
- *     quality: integer between 1 and 100
- */
 - (void)takePicture:(CDVInvokedUrlCommand*)command;
 - (void)cleanup:(CDVInvokedUrlCommand*)command;
 - (void)repositionPopover:(CDVInvokedUrlCommand*)command;
 
+// UIImagePickerControllerDelegate methods
 - (void)imagePickerController:(UIImagePickerController*)picker 
didFinishPickingMediaWithInfo:(NSDictionary*)info;
 - (void)imagePickerController:(UIImagePickerController*)picker 
didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo;
 - (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker;
+
+// UINavigationControllerDelegate method
 - (void)navigationController:(UINavigationController *)navigationController 
willShowViewController:(UIViewController *)viewController 
animated:(BOOL)animated;
 
+// CLLocationManagerDelegate methods
 - (void)locationManager:(CLLocationManager*)manager 
didUpdateToLocation:(CLLocation*)newLocation 
fromLocation:(CLLocation*)oldLocation;
 - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError 
*)error;
 
+// PHPickerViewController specific methods (iOS 14+)
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
+- (void)showPHPicker:(NSString*)callbackId 
withOptions:(CDVPictureOptions*)pictureOptions API_AVAILABLE(ios(14));
+- (void)processPHPickerImage:(UIImage*)image 
assetIdentifier:(NSString*)assetIdentifier callbackId:(NSString*)callbackId 
options:(CDVPictureOptions*)options API_AVAILABLE(ios(14));
+- (void)finalizePHPickerImage:(UIImage*)image metadata:(NSDictionary*)metadata 
callbackId:(NSString*)callbackId options:(CDVPictureOptions*)options 
API_AVAILABLE(ios(14));
+// PHPickerViewControllerDelegate method
+- (void)picker:(PHPickerViewController *)picker 
didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14));
+#endif
+
 @end
diff --git a/src/ios/CDVCamera.m b/src/ios/CDVCamera.m
index 9fb6c73..d1c417d 100644
--- a/src/ios/CDVCamera.m
+++ b/src/ios/CDVCamera.m
@@ -39,7 +39,8 @@
 
 static NSSet* org_apache_cordova_validArrowDirections;
 
-static NSString* toBase64(NSData* data) {
+static NSString* toBase64(NSData* data)
+{
     SEL s1 = NSSelectorFromString(@"cdv_base64EncodedString");
     SEL s2 = NSSelectorFromString(@"base64EncodedString");
     SEL s3 = NSSelectorFromString(@"base64EncodedStringWithOptions:");
@@ -63,7 +64,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 @implementation CDVPictureOptions
 
-+ (instancetype) createFromTakePictureArguments:(CDVInvokedUrlCommand*)command
++ (instancetype)createFromTakePictureArguments:(CDVInvokedUrlCommand*)command
 {
     CDVPictureOptions* pictureOptions = [[CDVPictureOptions alloc] init];
 
@@ -110,7 +111,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 
 @synthesize hasPendingOperation, pickerController, locationManager;
 
-- (NSURL*) urlTransformer:(NSURL*)url
+- (NSURL*)urlTransformer:(NSURL*)url
 {
     NSURL* urlToTransform = url;
 
@@ -140,6 +141,29 @@ static NSString* MIME_JPEG    = @"image/jpeg";
            (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
 }
 
+/**
+ Called by JS function navigator.camera.getPicture(cameraSuccess, cameraError, 
cameraOptions)
+ which will invoke the camera or photo picker to capture or select an image or 
video.
+ 
+ @param command A Cordova command whose arguments map to camera options:
+   - index 0 (quality): NSNumber (1–100). JPEG quality when encodingType is 
JPEG. Default: 50.
+   - index 1 (destinationType): NSNumber (DestinationType). File URI or Data 
URL. Default: File URI.
+   - index 2 (sourceType): NSNumber (UIImagePickerControllerSourceType). 
Camera or Photo Library. Default: Camera.
+   - index 3 (targetWidth): NSNumber (optional). Desired width for 
scaling/cropping.
+   - index 4 (targetHeight): NSNumber (optional). Desired height for 
scaling/cropping.
+   - index 5 (encodingType): NSNumber (EncodingType). JPEG or PNG. Default: 
JPEG.
+   - index 6 (mediaType): NSNumber (MediaType). Picture, Video, or All. 
Default: Picture.
+   - index 7 (allowsEditing): NSNumber(BOOL). Allow user to crop/edit. 
Default: NO.
+   - index 8 (correctOrientation): NSNumber(BOOL). Fix EXIF orientation. 
Default: NO.
+   - index 9 (saveToPhotoAlbum): NSNumber(BOOL). Save captured image to 
Photos. Default: NO.
+   - index 10 (popoverOptions): NSDictionary (iPad only). Popover positioning 
and sizing.
+   - index 11 (cameraDirection): NSNumber 
(UIImagePickerControllerCameraDevice). Front/Rear. Default: Rear.
+
+ @discussion
+ This method validates hardware availability and permissions (camera or photo 
library),
+ then presents the appropriate UI (UIImagePickerController or 
PHPickerViewController on iOS 14+).
+ The result is returned via the Cordova callback.
+ */
 - (void)takePicture:(CDVInvokedUrlCommand*)command
 {
     self.hasPendingOperation = YES;
@@ -151,67 +175,133 @@ static NSString* MIME_JPEG    = @"image/jpeg";
         pictureOptions.usesGeolocation = [weakSelf usesGeolocation];
         pictureOptions.cropToSize = NO;
 
-        BOOL hasCamera = [UIImagePickerController 
isSourceTypeAvailable:pictureOptions.sourceType];
-        if (!hasCamera) {
-            NSLog(@"Camera.getPicture: source type %lu not available.", 
(unsigned long)pictureOptions.sourceType);
-            CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No camera available"];
-            [weakSelf.commandDelegate sendPluginResult:result 
callbackId:command.callbackId];
-            return;
-        }
-
-        // Validate the app has permission to access the camera
+        // The camera should be used to take a picture
         if (pictureOptions.sourceType == 
UIImagePickerControllerSourceTypeCamera) {
-            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo 
completionHandler:^(BOOL granted)
-             {
-                 if (!granted)
-                 {
-                     // Denied; show an alert
-                     dispatch_async(dispatch_get_main_queue(), ^{
-                         UIAlertController *alertController = 
[UIAlertController alertControllerWithTitle:[[NSBundle mainBundle] 
objectForInfoDictionaryKey:@"CFBundleDisplayName"] 
message:NSLocalizedString(@"Access to the camera has been prohibited; please 
enable it in the Settings app to continue.", nil) 
preferredStyle:UIAlertControllerStyleAlert];
-                         [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault 
handler:^(UIAlertAction * _Nonnull action) {
-                             [weakSelf 
sendNoPermissionResult:command.callbackId];
-                         }]];
-                         [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"Settings", nil) 
style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
-                             [[UIApplication sharedApplication] openURL:[NSURL 
URLWithString:UIApplicationOpenSettingsURLString] options:@{} 
completionHandler:nil];
-                             [weakSelf 
sendNoPermissionResult:command.callbackId];
-                         }]];
-                         [weakSelf.viewController 
presentViewController:alertController animated:YES completion:nil];
-                     });
+            // Check if camera is available
+            if (![UIImagePickerController 
isSourceTypeAvailable:pictureOptions.sourceType]) {
+                NSLog(@"Camera.getPicture: source type %lu not available.", 
(unsigned long)pictureOptions.sourceType);
+                [weakSelf.commandDelegate sendPluginResult:[CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No camera available"]
+                                                callbackId:command.callbackId];
+                return;
+            }
+            
+            // Validate the app has permission to access the camera
+            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo 
completionHandler:^(BOOL granted) {
+                 // Show an alert if not granted
+                 if (!granted) {
+                     [weakSelf 
presentPermissionDeniedAlertWithMessage:@"Access to the camera has been 
prohibited; please enable it in the Settings app to continue."
+                                                            
callbackId:command.callbackId];
                  } else {
-                     dispatch_async(dispatch_get_main_queue(), ^{
-                         [weakSelf showCameraPicker:command.callbackId 
withOptions:pictureOptions];
-                     });
+                     [weakSelf showCameraPicker:command.callbackId 
withOptions:pictureOptions];
                  }
              }];
+            
+            // A photo should be picked from the photo library
         } else {
-            [weakSelf options:pictureOptions requestPhotoPermissions:^(BOOL 
granted) {
-                if (!granted) {
-                    // Denied; show an alert
-                    dispatch_async(dispatch_get_main_queue(), ^{
-                        UIAlertController *alertController = 
[UIAlertController alertControllerWithTitle:[[NSBundle mainBundle] 
objectForInfoDictionaryKey:@"CFBundleDisplayName"] 
message:NSLocalizedString(@"Access to the camera roll has been prohibited; 
please enable it in the Settings to continue.", nil) 
preferredStyle:UIAlertControllerStyleAlert];
-                        [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault 
handler:^(UIAlertAction * _Nonnull action) {
-                            [weakSelf 
sendNoPermissionResult:command.callbackId];
-                        }]];
-                        [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"Settings", nil) 
style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
-                            [[UIApplication sharedApplication] openURL:[NSURL 
URLWithString:UIApplicationOpenSettingsURLString]];
-                            [weakSelf 
sendNoPermissionResult:command.callbackId];
-                        }]];
-                        [weakSelf.viewController 
presentViewController:alertController animated:YES completion:nil];
-                    });
-                } else {
-                    dispatch_async(dispatch_get_main_queue(), ^{
+            // Use PHPickerViewController on iOS 14+
+            // Doesn't require permissions
+            if (@available(iOS 14, *)) {
+                    [weakSelf showCameraPicker:command.callbackId 
withOptions:pictureOptions];
+                
+                // On iOS < 14, use UIImagePickerController and request 
permissions
+            } else {
+                // Request permission
+                [weakSelf options:pictureOptions 
requestPhotoPermissions:^(BOOL granted) {
+                    if (!granted) {
+                        // Denied; show an alert
+                        [weakSelf 
presentPermissionDeniedAlertWithMessage:@"Access to the camera roll has been 
prohibited; please enable it in the Settings to continue."
+                                                               
callbackId:command.callbackId];
+                    } else {
                         [weakSelf showCameraPicker:command.callbackId 
withOptions:pictureOptions];
-                    });
-                }
-            }];
+                    }
+                }];
+            }
         }
     }];
 }
 
-- (void)showCameraPicker:(NSString*)callbackId withOptions:(CDVPictureOptions 
*) pictureOptions
+/**
+ Presents a permission denial alert with OK and Settings actions.
+ @param message The alert message to show.
+ @param callbackId The Cordova callback identifier to send an error if needed.
+ */
+- (void)presentPermissionDeniedAlertWithMessage:(NSString*)message 
callbackId:(NSString*)callbackId
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        
+        NSString *bundleDisplayName = [[NSBundle mainBundle] 
objectForInfoDictionaryKey:@"CFBundleDisplayName"];
+        UIAlertController *alertController = [UIAlertController 
alertControllerWithTitle:bundleDisplayName
+                                                                               
  message:NSLocalizedString(message, nil)
+                                                                          
preferredStyle:UIAlertControllerStyleAlert];
+        
+        // Add buttons
+        __weak CDVCamera *weakSelf = self;
+        
+        // Ok button
+        [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"OK", nil)
+                                                            
style:UIAlertActionStyleDefault
+                                                          
handler:^(UIAlertAction * _Nonnull action) {
+            [weakSelf sendNoPermissionResult:callbackId];
+        }]];
+        
+        // Button for open settings
+        [alertController addAction:[UIAlertAction 
actionWithTitle:NSLocalizedString(@"Settings", nil)
+                                                            
style:UIAlertActionStyleDefault
+                                                          
handler:^(UIAlertAction * _Nonnull action) {
+            // Open settings
+            [[UIApplication sharedApplication] openURL:[NSURL 
URLWithString:UIApplicationOpenSettingsURLString]
+                                               options:@{}
+                                     completionHandler:nil];
+            [weakSelf sendNoPermissionResult:callbackId];
+        }]];
+
+        [self.viewController presentViewController:alertController 
animated:YES completion:nil];
+    });
+}
+
+- (void)sendNoPermissionResult:(NSString*)callbackId
+{
+    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"has no access to 
camera"];   // error callback expects string ATM
+
+    [self.commandDelegate sendPluginResult:result callbackId:callbackId];
+    self.hasPendingOperation = NO;
+    self.pickerController = nil;
+}
+
+/**
+ Presents the appropriate UI to capture or select media based on the provided 
options and OS version.
+ 
+ On iOS 14 and later, when the source type is PHOTOLIBRARY (or 
SAVEDPHOTOALBUM), this method presents
+ PHPickerViewController to select media without requiring Photos 
authorization. Otherwise, it falls back
+ to UIImagePickerController for camera usage or on older iOS versions.
+ 
+ Threading:
+ - Ensures presentation occurs on the main thread.
+ 
+ Behavior:
+ - Dismisses any visible popover before presenting a new picker (iPad).
+ - Configures delegates, media types, and popover presentation as needed.
+ - Updates `hasPendingOperation` to reflect plugin activity state.
+ 
+ @param callbackId The Cordova callback identifier used to deliver results 
back to JavaScript.
+ @param pictureOptions Parsed camera options (sourceType, mediaType, 
allowsEditing, popoverOptions, etc.).
+ */
+- (void)showCameraPicker:(NSString*)callbackId 
withOptions:(CDVPictureOptions*)pictureOptions
 {
     // Perform UI operations on the main thread
     dispatch_async(dispatch_get_main_queue(), ^{
+        // Use PHPickerViewController for photo library on iOS 14+
+        if (@available(iOS 14, *)) {
+            // sourceType is PHOTOLIBRARY
+            if (pictureOptions.sourceType == 
UIImagePickerControllerSourceTypePhotoLibrary ||
+                // sourceType is SAVEDPHOTOALBUM (same as PHOTOLIBRARY)
+                pictureOptions.sourceType == 
UIImagePickerControllerSourceTypeSavedPhotosAlbum) {
+                [self showPHPicker:callbackId withOptions:pictureOptions];
+                return;
+            }
+        }
+        
+        // Use UIImagePickerController for camera or as image picker for iOS 
older than 14
         CDVCameraPicker* cameraPicker = [CDVCameraPicker 
createFromPictureOptions:pictureOptions];
         self.pickerController = cameraPicker;
 
@@ -235,22 +325,206 @@ static NSString* MIME_JPEG    = @"image/jpeg";
             self.hasPendingOperation = NO;
         } else {
             cameraPicker.modalPresentationStyle = 
UIModalPresentationCurrentContext;
-            [self.viewController presentViewController:cameraPicker 
animated:YES completion:^{
+            [self.viewController presentViewController:cameraPicker
+                                              animated:YES
+                                            completion:^{
                 self.hasPendingOperation = NO;
             }];
         }
     });
 }
 
-- (void)sendNoPermissionResult:(NSString*)callbackId
+// Since iOS 14, we can use PHPickerViewController to select images from the 
photo library
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
+- (void)showPHPicker:(NSString*)callbackId 
withOptions:(CDVPictureOptions*)pictureOptions API_AVAILABLE(ios(14))
 {
-    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"has no access to 
camera"];   // error callback expects string ATM
+    PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
+    
+    // Configure filter based on media type
+    // Images
+    if (pictureOptions.mediaType == MediaTypePicture) {
+        config.filter = [PHPickerFilter imagesFilter];
+        
+        // Videos
+    } else if (pictureOptions.mediaType == MediaTypeVideo) {
+        config.filter = [PHPickerFilter videosFilter];
+        
+        // Images and videos
+    } else if (pictureOptions.mediaType == MediaTypeAll) {
+        config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
+            [PHPickerFilter imagesFilter],
+            [PHPickerFilter videosFilter]
+        ]];
+    }
+    
+    config.selectionLimit = 1;
+    config.preferredAssetRepresentationMode = 
PHPickerConfigurationAssetRepresentationModeCurrent;
+    
+    PHPickerViewController *picker = [[PHPickerViewController alloc] 
initWithConfiguration:config];
+    picker.delegate = self;
+    
+    // Store callback ID and options in picker with objc_setAssociatedObject
+    // PHPickerViewController’s delegate method picker:didFinishPicking: only 
gives you back the picker instance
+    // and the results array. It doesn’t carry arbitrary context. By 
associating the callbackId and pictureOptions
+    // with the picker, you can retrieve them later inside the delegate method
+    objc_setAssociatedObject(picker, "callbackId", callbackId, 
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    objc_setAssociatedObject(picker, "pictureOptions", pictureOptions, 
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    
+    [self.viewController presentViewController:picker animated:YES 
completion:^{
+        self.hasPendingOperation = NO;
+    }];
+}
 
-    [self.commandDelegate sendPluginResult:result callbackId:callbackId];
+// PHPickerViewControllerDelegate method
+- (void)picker:(PHPickerViewController*)picker 
didFinishPicking:(NSArray<PHPickerResult*>*)results API_AVAILABLE(ios(14))
+{
+    NSString *callbackId = objc_getAssociatedObject(picker, "callbackId");
+    CDVPictureOptions *pictureOptions = objc_getAssociatedObject(picker, 
"pictureOptions");
+    
+    __weak CDVCamera* weakSelf = self;
+    
+    [picker dismissViewControllerAnimated:YES completion:^{
+        if (results.count == 0) {
+            // User cancelled
+            CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No Image Selected"];
+            [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
+            weakSelf.hasPendingOperation = NO;
+            return;
+        }
+        
+        PHPickerResult *pickerResult = results.firstObject;
+        
+        // Check if it's a video
+        if ([pickerResult.itemProvider 
hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) {
+            [pickerResult.itemProvider 
loadFileRepresentationForTypeIdentifier:UTTypeMovie.identifier 
completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
+                if (error) {
+                    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error 
localizedDescription]];
+                    [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
+                    weakSelf.hasPendingOperation = NO;
+                    return;
+                }
+                
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    NSString* videoPath = [weakSelf createTmpVideo:[url path]];
+                    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_OK messageAsString:videoPath];
+                    [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
+                    weakSelf.hasPendingOperation = NO;
+                });
+            }];
+            
+            // Handle image
+        } else if ([pickerResult.itemProvider canLoadObjectOfClass:[UIImage 
class]]) {
+            [pickerResult.itemProvider loadObjectOfClass:[UIImage class] 
completionHandler:^(__kindof id<NSItemProviderReading>  _Nullable object, 
NSError * _Nullable error) {
+                if (error) {
+                    CDVPluginResult* result = [CDVPluginResult 
resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error 
localizedDescription]];
+                    [weakSelf.commandDelegate sendPluginResult:result 
callbackId:callbackId];
+                    weakSelf.hasPendingOperation = NO;
+                    return;
+                }
+                
+                UIImage *image = (UIImage *)object;
+                
+                // Get asset identifier to fetch metadata
+                NSString *assetIdentifier = pickerResult.assetIdentifier;
+                
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    [weakSelf processPHPickerImage:image 
assetIdentifier:assetIdentifier callbackId:callbackId options:pictureOptions];
+                });
+            }];
+        }
+    }];
+}
 
-    self.hasPendingOperation = NO;
-    self.pickerController = nil;
+- (void)processPHPickerImage:(UIImage*)image
+             assetIdentifier:(NSString*)assetIdentifier
+                  callbackId:(NSString*)callbackId
+                     options:(CDVPictureOptions*)options API_AVAILABLE(ios(14))
+{
+    __weak CDVCamera* weakSelf = self;
+    
+    // Fetch metadata if asset identifier is available
+    if (assetIdentifier) {
+        PHFetchResult *result = [PHAsset 
fetchAssetsWithLocalIdentifiers:@[assetIdentifier] options:nil];
+        PHAsset *asset = result.firstObject;
+        
+        if (asset) {
+            PHImageRequestOptions *imageOptions = [[PHImageRequestOptions 
alloc] init];
+            imageOptions.synchronous = YES;
+            imageOptions.networkAccessAllowed = YES;
+            
+            [[PHImageManager defaultManager] 
requestImageDataAndOrientationForAsset:asset
+                                                                            
options:imageOptions
+                                                                      
resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, 
CGImagePropertyOrientation orientation, NSDictionary *_Nullable info) {
+                NSDictionary *metadata = imageData ? [weakSelf 
convertImageMetadata:imageData] : nil;
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    [weakSelf finalizePHPickerImage:image metadata:metadata 
callbackId:callbackId options:options];
+                });
+            }];
+            return;
+        }
+    }
+    
+    // No metadata available
+    [self finalizePHPickerImage:image metadata:nil callbackId:callbackId 
options:options];
+}
+
+- (void)finalizePHPickerImage:(UIImage*)image
+                     metadata:(NSDictionary*)metadata
+                   callbackId:(NSString*)callbackId
+                      options:(CDVPictureOptions*)options 
API_AVAILABLE(ios(14))
+{
+    // Process image according to options
+    UIImage *processedImage = image;
+    
+    if (options.correctOrientation) {
+        processedImage = [processedImage imageCorrectedForCaptureOrientation];
+    }
+    
+    if ((options.targetSize.width > 0) && (options.targetSize.height > 0)) {
+        if (options.cropToSize) {
+            processedImage = [processedImage 
imageByScalingAndCroppingForSize:options.targetSize];
+        } else {
+            processedImage = [processedImage 
imageByScalingNotCroppingForSize:options.targetSize];
+        }
+    }
+    
+    // Create info dictionary similar to UIImagePickerController
+    NSMutableDictionary *info = [NSMutableDictionary dictionary];
+    [info setObject:processedImage 
forKey:UIImagePickerControllerOriginalImage];
+    if (metadata) {
+        [info setObject:metadata 
forKey:@"UIImagePickerControllerMediaMetadata"];
+    }
+    
+    // Store metadata for processing
+    if (metadata) {
+        self.metadata = [[NSMutableDictionary alloc] init];
+        
+        NSMutableDictionary* EXIFDictionary = [[metadata 
objectForKey:(NSString*)kCGImagePropertyExifDictionary] mutableCopy];
+        if (EXIFDictionary) {
+            [self.metadata setObject:EXIFDictionary 
forKey:(NSString*)kCGImagePropertyExifDictionary];
+        }
+        
+        NSMutableDictionary* TIFFDictionary = [[metadata 
objectForKey:(NSString*)kCGImagePropertyTIFFDictionary] mutableCopy];
+        if (TIFFDictionary) {
+            [self.metadata setObject:TIFFDictionary 
forKey:(NSString*)kCGImagePropertyTIFFDictionary];
+        }
+        
+        NSMutableDictionary* GPSDictionary = [[metadata 
objectForKey:(NSString*)kCGImagePropertyGPSDictionary] mutableCopy];
+        if (GPSDictionary) {
+            [self.metadata setObject:GPSDictionary 
forKey:(NSString*)kCGImagePropertyGPSDictionary];
+        }
+    }
+    
+    __weak CDVCamera* weakSelf = self;
+    
+    // Process and return result
+    [self resultForImage:options info:info completion:^(CDVPluginResult* res) {
+        [weakSelf.commandDelegate sendPluginResult:res callbackId:callbackId];
+        weakSelf.hasPendingOperation = NO;
+        weakSelf.pickerController = nil;
+    }];
 }
+#endif
 
 - (void)repositionPopover:(CDVInvokedUrlCommand*)command
 {
@@ -301,25 +575,29 @@ static NSString* MIME_JPEG    = @"image/jpeg";
                                                                animated:YES];
 }
 
-- (void)navigationController:(UINavigationController *)navigationController 
willShowViewController:(UIViewController *)viewController 
animated:(BOOL)animated
+// UINavigationControllerDelegate method
+- (void)navigationController:(UINavigationController*)navigationController
+      willShowViewController:(UIViewController*)viewController
+                    animated:(BOOL)animated
 {
-    if([navigationController isKindOfClass:[UIImagePickerController class]]){
-        
-        // If popoverWidth and popoverHeight are specified and are greater 
than 0, then set popover size, else use apple's default popoverSize
+    if([navigationController isKindOfClass:[UIImagePickerController class]]) {
+        // If popoverWidth and popoverHeight are specified and are greater 
than 0,
+        // then set popover size, else use apple's default popoverSize
         NSDictionary* options = 
self.pickerController.pictureOptions.popoverOptions;
+        
         if(options) {
             NSInteger popoverWidth = [self integerValueForKey:options 
key:@"popoverWidth" defaultValue:0];
             NSInteger popoverHeight = [self integerValueForKey:options 
key:@"popoverHeight" defaultValue:0];
-            if(popoverWidth > 0 && popoverHeight > 0)
-            {
+            
+            if(popoverWidth > 0 && popoverHeight > 0) {
                 [viewController 
setPreferredContentSize:CGSizeMake(popoverWidth,popoverHeight)];
             }
         }
         
-        
-        UIImagePickerController* cameraPicker = 
(UIImagePickerController*)navigationController;
+        UIImagePickerController* imagePicker = 
(UIImagePickerController*)navigationController;
 
-        if(![cameraPicker.mediaTypes containsObject:(NSString*)kUTTypeImage]){
+        // Set "Videos" title if mediaType is not for images
+        if(![imagePicker.mediaTypes containsObject:(NSString*)kUTTypeImage]) {
             [viewController.navigationItem 
setTitle:NSLocalizedString(@"Videos", nil)];
         }
     }
@@ -375,7 +653,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     self.hasPendingOperation = NO;
 }
 
-- (NSString*) getMimeForEncoding:(CDVEncodingType) encoding {
+- (NSString*)getMimeForEncoding:(CDVEncodingType)encoding
+{
     switch (encoding) {
         case EncodingTypePNG: return MIME_PNG;
         case EncodingTypeJPEG:
@@ -384,7 +663,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     }
 }
 
-- (NSString*) formatAsDataURI:(NSData*) data withMIME:(NSString*) mime {
+- (NSString*)formatAsDataURI:(NSData*)data withMIME:(NSString*)mime
+{
     NSString* base64 = toBase64(data);
     
     if (base64 == nil) {
@@ -394,20 +674,20 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return [NSString stringWithFormat:@"data:%@;base64,%@", mime, base64];
 }
 
-- (NSString*) processImageAsDataUri:(UIImage*) image info:(NSDictionary*) info 
options:(CDVPictureOptions*) options
+- (NSString*)processImageAsDataUri:(UIImage*)image info:(NSDictionary*)info 
options:(CDVPictureOptions*)options
 {
     NSString* mime = nil;
-    NSData* data = [self processImage: image info: info options: options 
outMime: &mime];
+    NSData* data = [self processImage:image info:info options:options 
outMime:&mime];
     
-    return [self formatAsDataURI: data withMIME: mime];
+    return [self formatAsDataURI:data withMIME:mime];
 }
 
-- (NSData*) processImage:(UIImage*) image info:(NSDictionary*) info 
options:(CDVPictureOptions*) options
+- (NSData*)processImage:(UIImage*)image info:(NSDictionary*)info 
options:(CDVPictureOptions*)options
 {
-    return [self processImage:image  info: info options: options outMime: nil];
+    return [self processImage:image info:info options:options outMime:nil];
 }
 
-- (NSData*) processImage:(UIImage*)image info:(NSDictionary*)info 
options:(CDVPictureOptions*)options outMime:(NSString**) outMime
+- (NSData*)processImage:(UIImage*)image info:(NSDictionary*)info 
options:(CDVPictureOptions*)options outMime:(NSString**)outMime
 {
     NSData* data = nil;
 
@@ -484,8 +764,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 /* --------------------------------------------------------------
 -- get the metadata of the image from a PHAsset
 -------------------------------------------------------------- */
-- (NSDictionary*)getImageMetadataFromAsset:(PHAsset*)asset {
-
+- (NSDictionary*)getImageMetadataFromAsset:(PHAsset*)asset
+{
     if(asset == nil) {
         return nil;
     }
@@ -503,7 +783,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return dict;
 }
 
--(NSDictionary*)convertImageMetadata:(NSData*)imageData {
+- (NSDictionary*)convertImageMetadata:(NSData*)imageData
+{
     CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge 
CFDataRef)(imageData), NULL);
     if (imageSource) {
         NSDictionary *options = @{(NSString *)kCGImageSourceShouldCache : 
[NSNumber numberWithBool:NO]};
@@ -522,12 +803,20 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return nil;
 }
 
+/**
+ Requests Photos library permissions when needed for picking media from the 
photo library.
+ This is only needed for iOS 13 and older when using UIImagePickerController 
for picking an image.
+ On iOS 14 and later, PHPickerViewController is used and does not need extra 
permissions.
+ @param options The picture options indicating the requested source type.
+ @param completion A block invoked with YES when access is authorized (or not 
required),
+                   or NO when access is denied or restricted.
+ */
 - (void)options:(CDVPictureOptions*)options requestPhotoPermissions:(void 
(^)(BOOL auth))completion
 {
-    if((unsigned long)options.sourceType == 1){
+    // This is would be no good response
+    if(options.sourceType == UIImagePickerControllerSourceTypeCamera) {
         completion(YES);
-    }
-    else{
+    } else {
         PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
 
         switch (status) {
@@ -592,7 +881,9 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return (scaledImage == nil ? image : scaledImage);
 }
 
-- (void)resultForImage:(CDVPictureOptions*)options info:(NSDictionary*)info 
completion:(void (^)(CDVPluginResult* res))completion
+- (void)resultForImage:(CDVPictureOptions*)options
+                  info:(NSDictionary*)info
+            completion:(void (^)(CDVPluginResult* res))completion
 {
     CDVPluginResult* result = nil;
     BOOL saveToPhotoAlbum = options.saveToPhotoAlbum;
@@ -678,7 +969,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 
messageAsString:moviePath];
 }
 
-- (NSString *) createTmpVideo:(NSString *) moviePath {
+- (NSString*)createTmpVideo:(NSString*)moviePath
+{
     NSString* moviePathExtension = [moviePath pathExtension];
     NSString* copyMoviePath = [self tempFilePath:moviePathExtension];
     NSFileManager* fileMgr = [[NSFileManager alloc] init];
@@ -687,6 +979,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return [[NSURL fileURLWithPath:copyMoviePath] absoluteString];
 }
 
+#pragma mark UIImagePickerControllerDelegate methods
+
 - (void)imagePickerController:(UIImagePickerController*)picker 
didFinishPickingMediaWithInfo:(NSDictionary*)info
 {
     __weak CDVCameraPicker* cameraPicker = (CDVCameraPicker*)picker;
@@ -724,7 +1018,9 @@ static NSString* MIME_JPEG    = @"image/jpeg";
 }
 
 // older api calls newer didFinishPickingMediaWithInfo
-- (void)imagePickerController:(UIImagePickerController*)picker 
didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo
+- (void)imagePickerController:(UIImagePickerController*)picker
+        didFinishPickingImage:(UIImage*)image
+                  editingInfo:(NSDictionary*)editingInfo
 {
     NSDictionary* imageInfo = [NSDictionary dictionaryWithObject:image 
forKey:UIImagePickerControllerOriginalImage];
 
@@ -754,6 +1050,8 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     [[cameraPicker presentingViewController] dismissViewControllerAnimated:YES 
completion:invoke];
 }
 
+#pragma mark CLLocationManager
+
 - (CLLocationManager*)locationManager
 {
     if (locationManager != nil) {
@@ -767,7 +1065,11 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     return locationManager;
 }
 
-- (void)locationManager:(CLLocationManager*)manager 
didUpdateToLocation:(CLLocation*)newLocation 
fromLocation:(CLLocation*)oldLocation
+# pragma mark CLLocationManagerDelegate methods
+
+- (void)locationManager:(CLLocationManager*)manager
+    didUpdateToLocation:(CLLocation*)newLocation
+           fromLocation:(CLLocation*)oldLocation
 {
     if (locationManager == nil) {
         return;
@@ -922,7 +1224,7 @@ static NSString* MIME_JPEG    = @"image/jpeg";
     [super viewWillAppear:animated];
 }
 
-+ (instancetype) createFromPictureOptions:(CDVPictureOptions*)pictureOptions;
++ (instancetype)createFromPictureOptions:(CDVPictureOptions*)pictureOptions
 {
     CDVCameraPicker* cameraPicker = [[CDVCameraPicker alloc] init];
     cameraPicker.pictureOptions = pictureOptions;
diff --git a/tests/ios/CDVCameraTest/CDVCameraLibTests/CameraTest.m 
b/tests/ios/CDVCameraTest/CDVCameraLibTests/CameraTest.m
index d74cdba..13a71ad 100644
--- a/tests/ios/CDVCameraTest/CDVCameraLibTests/CameraTest.m
+++ b/tests/ios/CDVCameraTest/CDVCameraLibTests/CameraTest.m
@@ -37,24 +37,27 @@
 - (UIImage*)retrieveImage:(NSDictionary*)info 
options:(CDVPictureOptions*)options;
 - (CDVPluginResult*)resultForImage:(CDVPictureOptions*)options 
info:(NSDictionary*)info;
 - (CDVPluginResult*)resultForVideo:(NSDictionary*)info;
+- (NSDictionary*)convertImageMetadata:(NSData*)imageData;
 
 @end
 
 @implementation CameraTest
 
-- (void)setUp {
+- (void)setUp
+{
     [super setUp];
     // Put setup code here. This method is called before the invocation of 
each test method in the class.
     
     self.plugin = [[CDVCamera alloc] init];
 }
 
-- (void)tearDown {
+- (void)tearDown
+{
     // Put teardown code here. This method is called after the invocation of 
each test method in the class.
     [super tearDown];
 }
 
-- (void) testPictureOptionsCreate
+- (void)testPictureOptionsCreate
 {
     NSArray* args;
     CDVPictureOptions* options;
@@ -118,14 +121,14 @@
     XCTAssertEqual(options.usesGeolocation, NO);
 }
 
-- (void) testCameraPickerCreate
+- (void)testCameraPickerCreate
 {
     NSDictionary* popoverOptions;
     NSArray* args;
     CDVPictureOptions* pictureOptions;
     CDVCameraPicker* picker;
     
-    // Souce is Camera, and image type
+    // Source is Camera, and image type - Camera always uses 
UIImagePickerController
     
     popoverOptions = @{ @"x" : @1, @"y" : @2, @"width" : @3, @"height" : @4, 
@"popoverWidth": @200, @"popoverHeight": @300 };
     args = @[
@@ -157,7 +160,7 @@
         XCTAssertEqual(picker.cameraDevice, pictureOptions.cameraDirection);
     }
 
-    // Souce is not Camera, and all media types
+    // Source is Photo Library, and all media types - On iOS 14+ uses 
PHPicker, below uses UIImagePickerController
 
     args = @[
           @(49),
@@ -187,7 +190,7 @@
          XCTAssertEqualObjects(picker.mediaTypes, [UIImagePickerController 
availableMediaTypesForSourceType:picker.sourceType]);
     }
     
-    // Souce is not Camera, and either Image or Movie media type
+    // Source is Photo Library, and either Image or Movie media type - On iOS 
14+ uses PHPicker
 
     args = @[
              @(49),
@@ -218,7 +221,8 @@
     }
 }
 
-- (UIImage*) createImage:(CGRect)rect 
orientation:(UIImageOrientation)imageOrientation {
+- (UIImage*)createImage:(CGRect)rect 
orientation:(UIImageOrientation)imageOrientation
+{
     UIGraphicsBeginImageContext(rect.size);
     CGContextRef context = UIGraphicsGetCurrentContext();
     
@@ -233,8 +237,8 @@
     return image;
 }
 
-- (void) testImageScaleCropForSize {
-    
+- (void)testImageScaleCropForSize
+{
     UIImage *sourceImagePortrait, *sourceImageLandscape, *targetImage;
     CGSize targetSize = CGSizeZero;
     
@@ -279,7 +283,8 @@
     XCTAssertEqual(targetImage.size.height, targetSize.height);
 }
 
-- (void) testImageScaleNoCropForSize {
+- (void)testImageScaleNoCropForSize
+{
     UIImage *sourceImagePortrait, *sourceImageLandscape, *targetImage;
     CGSize targetSize = CGSizeZero;
     
@@ -330,7 +335,8 @@
     XCTAssertEqual(targetImage.size.height, targetSize.height);
 }
 
-- (void) testImageCorrectedForOrientation {
+- (void)testImageCorrectedForOrientation
+{
     UIImage *sourceImagePortrait, *sourceImageLandscape, *targetImage;
     CGSize targetSize = CGSizeZero;
     
@@ -383,7 +389,7 @@
 }
 
 
-- (void) testRetrieveImage
+- (void)testRetrieveImage
 {
     CDVPictureOptions* pictureOptions = [[CDVPictureOptions alloc] init];
     NSDictionary *infoDict1, *infoDict2;
@@ -461,7 +467,7 @@
     XCTAssertEqual(resultImage.size.height, scaledImageWithCrop.size.height);
 }
 
-- (void) testProcessImage
+- (void)testProcessImage
 {
     CDVPictureOptions* pictureOptions = [[CDVPictureOptions alloc] init];
     NSData* resultData;
@@ -508,4 +514,123 @@
     // TODO: usesGeolocation is not tested
 }
 
+#pragma mark - PHPickerViewController Tests (iOS 14+)
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 // Always true on XCode12+
+- (void)testPHPickerAvailability API_AVAILABLE(ios(14))
+{
+    XCTAssertNotNil([PHPickerViewController class]);
+    
+    PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
+    XCTAssertNotNil(config);
+    
+    config.filter = [PHPickerFilter imagesFilter];
+    XCTAssertNotNil(config.filter);
+    
+    PHPickerViewController *picker = [[PHPickerViewController alloc] 
initWithConfiguration:config];
+    XCTAssertNotNil(picker);
+}
+
+- (void)testPHPickerConfiguration API_AVAILABLE(ios(14))
+{
+    // Test image filter configuration
+    PHPickerConfiguration *imageConfig = [[PHPickerConfiguration alloc] init];
+    imageConfig.filter = [PHPickerFilter imagesFilter];
+    imageConfig.selectionLimit = 1;
+    
+    XCTAssertNotNil(imageConfig);
+    XCTAssertEqual(imageConfig.selectionLimit, 1);
+    
+    // Test video filter configuration
+    PHPickerConfiguration *videoConfig = [[PHPickerConfiguration alloc] init];
+    videoConfig.filter = [PHPickerFilter videosFilter];
+    
+    XCTAssertNotNil(videoConfig.filter);
+    
+    // Test all media types configuration
+    PHPickerConfiguration *allConfig = [[PHPickerConfiguration alloc] init];
+    allConfig.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
+        [PHPickerFilter imagesFilter],
+        [PHPickerFilter videosFilter]
+    ]];
+    
+    XCTAssertNotNil(allConfig.filter);
+}
+
+- (void)testPHPickerDelegateConformance API_AVAILABLE(ios(14))
+{
+    // Test that CDVCamera conforms to PHPickerViewControllerDelegate
+    XCTAssertTrue([self.plugin 
conformsToProtocol:@protocol(PHPickerViewControllerDelegate)]);
+    
+    // Test that the delegate method is implemented
+    SEL delegateSelector = @selector(picker:didFinishPicking:);
+    XCTAssertTrue([self.plugin respondsToSelector:delegateSelector]);
+}
+
+- (void)testShowPHPickerMethod API_AVAILABLE(ios(14))
+{
+    // Test that showPHPicker method exists
+    SEL showPHPickerSelector = @selector(showPHPicker:withOptions:);
+    XCTAssertTrue([self.plugin respondsToSelector:showPHPickerSelector]);
+    
+    // Test that processPHPickerImage method exists
+    SEL processSelector = 
@selector(processPHPickerImage:assetIdentifier:callbackId:options:);
+    XCTAssertTrue([self.plugin respondsToSelector:processSelector]);
+    
+    // Test that finalizePHPickerImage method exists
+    SEL finalizeSelector = 
@selector(finalizePHPickerImage:metadata:callbackId:options:);
+    XCTAssertTrue([self.plugin respondsToSelector:finalizeSelector]);
+}
+#endif
+
+- (void)testConvertImageMetadata
+{
+    // Create a test image
+    UIImage* testImage = [self createImage:CGRectMake(0, 0, 100, 100) 
orientation:UIImageOrientationUp];
+    NSData* imageData = UIImageJPEGRepresentation(testImage, 1.0);
+    
+    XCTAssertNotNil(imageData);
+    
+    // Test metadata conversion
+    NSDictionary* metadata = [self.plugin convertImageMetadata:imageData];
+    
+    // Metadata may be nil for generated images, but the method should not 
crash
+    // Real camera images would have EXIF data
+    XCTAssertTrue(metadata == nil || [metadata isKindOfClass:[NSDictionary 
class]]);
+}
+
+- (void)testPictureOptionsForPHPicker
+{
+    NSArray* args;
+    CDVPictureOptions* options;
+    
+    // Test options configuration for photo library (which would use PHPicker 
on iOS 14+)
+    args = @[
+             @(75),
+             @(DestinationTypeFileUri),
+             @(UIImagePickerControllerSourceTypePhotoLibrary),
+             @(800),
+             @(600),
+             @(EncodingTypeJPEG),
+             @(MediaTypePicture),
+             @NO,
+             @YES,
+             @NO,
+             [NSNull null],
+             @(UIImagePickerControllerCameraDeviceRear),
+             ];
+    
+    CDVInvokedUrlCommand* command = [[CDVInvokedUrlCommand alloc] 
initWithArguments:args callbackId:@"dummy" className:@"myclassname" 
methodName:@"mymethodname"];
+    options = [CDVPictureOptions createFromTakePictureArguments:command];
+    
+    // Verify options are correctly set for photo library source
+    XCTAssertEqual(options.sourceType, 
(int)UIImagePickerControllerSourceTypePhotoLibrary);
+    XCTAssertEqual([options.quality intValue], 75);
+    XCTAssertEqual(options.destinationType, (int)DestinationTypeFileUri);
+    XCTAssertEqual(options.targetSize.width, 800);
+    XCTAssertEqual(options.targetSize.height, 600);
+    XCTAssertEqual(options.correctOrientation, YES);
+    XCTAssertEqual(options.mediaType, (int)MediaTypePicture);
+}
+
 @end
diff --git a/types/index.d.ts b/types/index.d.ts
index 1d32f40..cbf62e2 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -58,7 +58,7 @@ interface CameraOptions {
      * Defined in navigator.camera.PictureSourceType. Default is CAMERA.
      *      PHOTOLIBRARY : 0,
      *      CAMERA : 1,
-     *      SAVEDPHOTOALBUM : 2
+     *      SAVEDPHOTOALBUM : 2 // same as PHOTOLIBRARY, kept for backward 
compatibility
      */
     sourceType?: number;
     /** Allow simple editing of image before selection. */
diff --git a/www/CameraConstants.js b/www/CameraConstants.js
index 32fe6a3..bd1adfc 100644
--- a/www/CameraConstants.js
+++ b/www/CameraConstants.js
@@ -62,11 +62,17 @@ module.exports = {
      * @enum {number}
      */
     PictureSourceType: {
-        /** Choose image from the device's photo library (same as 
SAVEDPHOTOALBUM for Android) */
+        /**
+         * Choose image from the device's photo library.
+         **/
         PHOTOLIBRARY: 0,
         /** Take picture from camera */
         CAMERA: 1,
-        /** Choose image only from the device's Camera Roll album (same as 
PHOTOLIBRARY for Android) */
+        /**
+         * Same as PHOTOLIBRARY, when running on Android, or iOS 14+.
+         * On iOS older than 14, an image can only be chosen from the device's 
Camera Roll album
+         * with this setting.
+         **/
         SAVEDPHOTOALBUM: 2
     },
     /**


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to