diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index 29fb408f8b45..044b78e3d5f0 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.1.3
+
+* Updates README to include a reference to the macOS PHPicker feature.
+
## 1.1.2
* Adds comment for the limit parameter.
diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md
index 866adf58118e..ef91fa400236 100755
--- a/packages/image_picker/image_picker/README.md
+++ b/packages/image_picker/image_picker/README.md
@@ -153,7 +153,7 @@ encourage the community to build packages that implement
#### macOS installation
-Since the macOS implementation uses `file_selector`, you will need to
+Since the macOS implementation uses `file_selector` by default, you will need to
add a filesystem access
[entitlement](https://flutter.dev/to/macos-entitlements):
@@ -162,6 +162,17 @@ add a filesystem access
```
+This setup is still required when using the [macOS PHPicker](#macos-phpicker) on **macOS 12 and older versions**, since it's only supported on **macOS 13+** and will fallback to the `file_selector` implementation.
+
+#### macOS PHPicker
+
+To use the [macOS native image picker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) which is supported on **macOS 13 and newer versions**,
+refer to the [image_picker_macos PHPicker](https://pub.dev/packages/image_picker_macos#phpicker) section.
+
+* **on macOS 13 and newer versions**: If this feature is used, the
+filesystem access entitlement in the [macOS installation](#macos-installation) is not required.
+* **on macOS 12 and older versions**: This feature is unsupported and will fallback to `file_selector` implementation, the filesystem access entitlement in the [macOS installation](#macos-installation) is required.
+
### Example
diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml
index 6c5676a60e0d..ad021b4c058c 100755
--- a/packages/image_picker/image_picker/pubspec.yaml
+++ b/packages/image_picker/image_picker/pubspec.yaml
@@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image
library, and taking new pictures with the camera.
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 1.1.2
+version: 1.1.3
environment:
sdk: ^3.3.0
diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md
index 6012fff8dbcd..7a3e305eb1fa 100644
--- a/packages/image_picker/image_picker_macos/CHANGELOG.md
+++ b/packages/image_picker/image_picker_macos/CHANGELOG.md
@@ -1,6 +1,7 @@
-## NEXT
+## 0.3.0
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
+* Adds macOS 13+ PHPicker functionality (optional and disabled by default).
## 0.2.1+1
diff --git a/packages/image_picker/image_picker_macos/README.md b/packages/image_picker/image_picker_macos/README.md
index 9aa87453532e..d9da5ce328c0 100644
--- a/packages/image_picker/image_picker_macos/README.md
+++ b/packages/image_picker/image_picker_macos/README.md
@@ -2,15 +2,45 @@
A macOS implementation of [`image_picker`][1].
+## PHPicker
+
+macOS 13.0 and newer versions supports native image picking via [PHPickerViewController][5].
+
+To use this feature, add the following code to your app before calling any `image_picker` APIs:
+
+
+```dart
+import 'package:image_picker_macos/image_picker_macos.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+// ···
+ final ImagePickerPlatform imagePickerImplementation =
+ ImagePickerPlatform.instance;
+ if (imagePickerImplementation is ImagePickerMacOS) {
+ imagePickerImplementation.useMacOSPHPicker = true;
+ }
+```
+
+This implementation depends on the photos in the [Photos for macOS App][6],
+if the user didn't open the app or import any photos to the app,
+they will see: `No photos` or `No Photos or Videos` message even if they
+have them as files on their desktop. The macOS Photos app supports importing images from an iOS device.
+
+> [!NOTE]
+> This feature is only supported on **macOS 13.0 and newer versions**, on older versions it will fallback to using [file_selector][3] if enabled.
+> By defaults it's disabled on all versions.
+
## Limitations
`ImageSource.camera` is not supported unless a `cameraDelegate` is set.
### pickImage()
-The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported.
+
+The arguments `maxWidth`, `maxHeight`, `imageQuality` and `limit` are only supported when using the [PHPicker](#phpicker) implementation; they are not available in the default [file_selector][5] implementation.
+
+The argument `requestFullMetadata` is unsupported on macOS.
### pickVideo()
-The argument `maxDuration` is not currently supported.
+The argument `maxDuration` is not supported even when using the [PHPicker](#phpicker) implementation.
## Usage
@@ -25,14 +55,18 @@ should add it to your `pubspec.yaml` as usual.
### Entitlements
-This package is currently implemented using [`file_selector`][3], so you will
-need to add a read-only file acces [entitlement][4]:
+This package’s default implementation relies on [file_selector][3],
+which requires the following read-only file access entitlement:
```xml
com.apple.security.files.user-selected.read-only
```
+If you're using the [PHPicker](#phpicker) and require at **least macOS 13** to run the app, this entitlement is not required.
+
[1]: https://pub.dev/packages/image_picker
[2]: https://flutter.dev/to/endorsed-federated-plugin
[3]: https://pub.dev/packages/file_selector
[4]: https://flutter.dev/to/macos-entitlements
+[5]: https://developer.apple.com/documentation/photokit/phpickerviewcontroller
+[6]: https://www.apple.com/in/macos/photos/
diff --git a/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart
new file mode 100644
index 000000000000..921d3448aa17
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart
@@ -0,0 +1,62 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:example/main.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:image_picker_macos/image_picker_macos.dart';
+import 'package:image_picker_macos/src/messages.g.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+import 'package:integration_test/integration_test.dart';
+
+ImagePickerMacOS get requireMacOSImplementation {
+ final ImagePickerPlatform imagePickerImplementation =
+ ImagePickerPlatform.instance;
+ if (imagePickerImplementation is! ImagePickerMacOS) {
+ fail('Expected the implementation to be $ImagePickerMacOS');
+ }
+ return imagePickerImplementation;
+}
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+ group('example', () {
+ testWidgets(
+ 'Pressing the PHPicker toggle button updates it correctly',
+ (WidgetTester tester) async {
+ final ImagePickerMacOS imagePickerImplementation =
+ requireMacOSImplementation;
+ expect(imagePickerImplementation.useMacOSPHPicker, false,
+ reason: 'The default is to not using PHPicker');
+
+ await tester.pumpWidget(const MyApp());
+ final Finder togglePHPickerFinder =
+ find.byTooltip('toggle macOS PHPPicker');
+ expect(togglePHPickerFinder, findsOneWidget);
+
+ await tester.tap(togglePHPickerFinder);
+ expect(imagePickerImplementation.useMacOSPHPicker, true,
+ reason: 'Pressing the toggle button should update it correctly');
+
+ await tester.tap(togglePHPickerFinder);
+ expect(imagePickerImplementation.useMacOSPHPicker, false,
+ reason: 'Pressing the toggle button should update it correctly');
+ },
+ );
+ testWidgets(
+ 'multi-video selection is not implemented',
+ (WidgetTester tester) async {
+ final ImagePickerApi hostApi = ImagePickerApi();
+ await expectLater(
+ hostApi.pickVideos(GeneralOptions(limit: 2)),
+ throwsA(predicate(
+ (PlatformException e) =>
+ e.code == 'UNIMPLEMENTED' &&
+ e.message == 'Multi-video selection is not implemented',
+ )),
+ );
+ },
+ );
+ });
+}
diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart
index 8f4887095c13..76da5f6e7f4d 100644
--- a/packages/image_picker/image_picker_macos/example/lib/main.dart
+++ b/packages/image_picker/image_picker_macos/example/lib/main.dart
@@ -8,11 +8,22 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
+// #docregion phpicker-example
+import 'package:image_picker_macos/image_picker_macos.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+// #enddocregion phpicker-example
import 'package:mime/mime.dart';
import 'package:video_player/video_player.dart';
void main() {
+ // Set to use macOS PHPicker.
+ // #docregion phpicker-example
+ final ImagePickerPlatform imagePickerImplementation =
+ ImagePickerPlatform.instance;
+ if (imagePickerImplementation is ImagePickerMacOS) {
+ imagePickerImplementation.useMacOSPHPicker = true;
+ }
+ // #enddocregion phpicker-example
runApp(const MyApp());
}
@@ -385,6 +396,46 @@ class _MyHomePageState extends State {
child: const Icon(Icons.videocam),
),
),
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: FloatingActionButton(
+ onPressed: () {
+ void showSnackbarText(String text) {
+ ScaffoldMessenger.of(context)
+ ..clearSnackBars()
+ ..showSnackBar(
+ SnackBar(content: Text(text)),
+ );
+ }
+
+ if (_picker is! ImagePickerMacOS) {
+ throw StateError(
+ 'Expected the implementation to be $ImagePickerMacOS but was ${_picker.runtimeType}');
+ }
+
+ if (_picker.useMacOSPHPicker) {
+ _picker.useMacOSPHPicker = false;
+ setState(() {});
+ showSnackbarText('Switched to file_picker implementation.');
+ } else {
+ _picker.useMacOSPHPicker = true;
+ setState(() {});
+ showSnackbarText(
+ 'Switched to macOS PHPPicker implementation.');
+ }
+ },
+ tooltip: 'toggle macOS PHPPicker',
+ child: () {
+ if (_picker is ImagePickerMacOS) {
+ return _picker.useMacOSPHPicker
+ ? const Icon(Icons.apple)
+ : const Icon(Icons.file_open);
+ }
+ throw StateError(
+ 'Expected the implementation to be $ImagePickerMacOS but was ${_picker.runtimeType}');
+ }(),
+ ),
+ ),
],
),
);
diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj
index 11508d6d349a..cd915eaea3a9 100644
--- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj
+++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj
@@ -27,6 +27,11 @@
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
38BD9D1FDAF3360EC1CC0018 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E9F2DE12CD9DE067306B460 /* Pods_Runner.framework */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
+ 7ABC95832CBD9D810004CBA6 /* ImageCompressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */; };
+ 7ABC95842CBD9D810004CBA6 /* ImageResizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */; };
+ 7ABC95852CBD9D810004CBA6 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */; };
+ 7ABC95892CBD9D8A0004CBA6 /* RunnerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -37,6 +42,20 @@
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
+ 7ABC952F2CB979800004CBA6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
+ 7ABC954F2CBAF9680004CBA6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -70,6 +89,12 @@
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
6D83DCCDFE2A45D91B8A2673 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompressTests.swift; sourceTree = ""; };
+ 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageResizeTests.swift; sourceTree = ""; };
+ 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerUITests.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
CD373636C90085FB713CE436 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
@@ -80,10 +105,25 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
38BD9D1FDAF3360EC1CC0018 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 7ABC95282CB979800004CBA6 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 7ABC95462CBAF9680004CBA6 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -94,7 +134,6 @@
103E92CF1EBAAB82E57C06F1 /* Pods-Runner.release.xcconfig */,
CD373636C90085FB713CE436 /* Pods-Runner.profile.xcconfig */,
);
- name = Pods;
path = Pods;
sourceTree = "";
};
@@ -114,6 +153,8 @@
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
+ 7ABC95822CBD9D810004CBA6 /* RunnerTests */,
+ 7ABC95882CBD9D8A0004CBA6 /* RunnerUITests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
0C0105042ACC016BCC44F609 /* Pods */,
@@ -124,6 +165,8 @@
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* example.app */,
+ 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */,
+ 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -163,6 +206,24 @@
path = Runner;
sourceTree = "";
};
+ 7ABC95822CBD9D810004CBA6 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 7ABC957F2CBD9D810004CBA6 /* ImageCompressTests.swift */,
+ 7ABC95802CBD9D810004CBA6 /* ImageResizeTests.swift */,
+ 7ABC95812CBD9D810004CBA6 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 7ABC95882CBD9D8A0004CBA6 /* RunnerUITests */ = {
+ isa = PBXGroup;
+ children = (
+ 7ABC95862CBD9D8A0004CBA6 /* RunnerUITests.swift */,
+ );
+ path = RunnerUITests;
+ sourceTree = "";
+ };
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -184,7 +245,6 @@
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
- 47A625D09D635034DC1B5C5E /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -192,17 +252,56 @@
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* example.app */;
productType = "com.apple.product-type.application";
};
+ 7ABC952A2CB979800004CBA6 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 7ABC95342CB979800004CBA6 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 7ABC95272CB979800004CBA6 /* Sources */,
+ 7ABC95282CB979800004CBA6 /* Frameworks */,
+ 7ABC95292CB979800004CBA6 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 7ABC95302CB979800004CBA6 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 7ABC952B2CB979800004CBA6 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 7ABC95482CBAF9680004CBA6 /* RunnerUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 7ABC95542CBAF9680004CBA6 /* Build configuration list for PBXNativeTarget "RunnerUITests" */;
+ buildPhases = (
+ 7ABC95452CBAF9680004CBA6 /* Sources */,
+ 7ABC95462CBAF9680004CBA6 /* Frameworks */,
+ 7ABC95472CBAF9680004CBA6 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 7ABC95502CBAF9680004CBA6 /* PBXTargetDependency */,
+ );
+ name = RunnerUITests;
+ productName = RunnerUITests;
+ productReference = 7ABC95492CBAF9680004CBA6 /* RunnerUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
- LastSwiftUpdateCheck = 0920;
+ LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -220,6 +319,15 @@
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
+ 7ABC952A2CB979800004CBA6 = {
+ CreatedOnToolsVersion = 16.0;
+ LastSwiftMigration = 1600;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
+ 7ABC95482CBAF9680004CBA6 = {
+ CreatedOnToolsVersion = 16.0;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
@@ -231,12 +339,17 @@
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ );
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ 7ABC952A2CB979800004CBA6 /* RunnerTests */,
+ 7ABC95482CBAF9680004CBA6 /* RunnerUITests */,
);
};
/* End PBXProject section */
@@ -251,6 +364,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 7ABC95292CB979800004CBA6 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 7ABC95472CBAF9680004CBA6 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -292,23 +419,6 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
- 47A625D09D635034DC1B5C5E /* [CP] Embed Pods Frameworks */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Embed Pods Frameworks";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
- showEnvVarsInLog = 0;
- };
DFDF70D0C2243DC8DB792B23 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -344,6 +454,24 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 7ABC95272CB979800004CBA6 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 7ABC95832CBD9D810004CBA6 /* ImageCompressTests.swift in Sources */,
+ 7ABC95842CBD9D810004CBA6 /* ImageResizeTests.swift in Sources */,
+ 7ABC95852CBD9D810004CBA6 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 7ABC95452CBAF9680004CBA6 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 7ABC95892CBD9D8A0004CBA6 /* RunnerUITests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -352,6 +480,16 @@
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
+ 7ABC95302CB979800004CBA6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 7ABC952F2CB979800004CBA6 /* PBXContainerItemProxy */;
+ };
+ 7ABC95502CBAF9680004CBA6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 7ABC954F2CBAF9680004CBA6 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -594,6 +732,137 @@
};
name = Release;
};
+ 7ABC95312CB979800004CBA6 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example";
+ };
+ name = Debug;
+ };
+ 7ABC95322CB979800004CBA6 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example";
+ };
+ name = Release;
+ };
+ 7ABC95332CB979800004CBA6 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example";
+ };
+ name = Profile;
+ };
+ 7ABC95512CBAF9680004CBA6 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = Runner;
+ };
+ name = Debug;
+ };
+ 7ABC95522CBAF9680004CBA6 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = Runner;
+ };
+ name = Release;
+ };
+ 7ABC95532CBAF9680004CBA6 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = Runner;
+ };
+ name = Profile;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -627,7 +896,41 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 7ABC95342CB979800004CBA6 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7ABC95312CB979800004CBA6 /* Debug */,
+ 7ABC95322CB979800004CBA6 /* Release */,
+ 7ABC95332CB979800004CBA6 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 7ABC95542CBAF9680004CBA6 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7ABC95512CBAF9680004CBA6 /* Debug */,
+ 7ABC95522CBAF9680004CBA6 /* Release */,
+ 7ABC95532CBAF9680004CBA6 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 5b055a3a376e..692345dd1b94 100644
--- a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -5,6 +5,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Bool {
return true
}
+
+ override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+ return true
+ }
}
diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift
new file mode 100644
index 000000000000..a3fa3152dfd8
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageCompressTests.swift
@@ -0,0 +1,37 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import XCTest
+
+@testable import image_picker_macos
+
+final class ImageCompressTests: XCTestCase {
+
+ private func createTestImage(size: NSSize) -> NSImage {
+ let image = NSImage(size: size)
+ image.lockFocus()
+ NSColor.white.set()
+ NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
+ image.unlockFocus()
+ return image
+ }
+
+ func testShouldCompressImage() {
+ XCTAssertFalse(shouldCompressImage(quality: 100), "Quality 100 should not compress the image.")
+ XCTAssertTrue(shouldCompressImage(quality: 80), "Quality bellow 100 should compress the image.")
+ XCTAssertFalse(
+ shouldCompressImage(quality: nil), "Should not compress the image when the quality is nil.")
+ }
+
+ func testImageCompression() throws {
+ let testImage = createTestImage(size: NSSize(width: 100, height: 100))
+
+ let compressedImage = try testImage.compressed(quality: 80)
+
+ XCTAssertLessThan(
+ compressedImage.tiffRepresentation!.count, testImage.tiffRepresentation!.count,
+ "Compressed image data should be smaller than the original image data.")
+ }
+
+}
diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift
new file mode 100644
index 000000000000..a6a331d82ebc
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/ImageResizeTests.swift
@@ -0,0 +1,236 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import XCTest
+
+@testable import image_picker_macos
+
+final class ImageResizeTests: XCTestCase {
+
+ private func createTestImage(size: NSSize) -> NSImage {
+ let image = NSImage(size: size)
+ image.lockFocus()
+ NSColor.black.set()
+ NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
+ image.unlockFocus()
+ return image
+ }
+
+ func testNilMaxSize() {
+ let originalImage = createTestImage(size: NSSize(width: 1200, height: 800))
+
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: nil)
+
+ XCTAssertEqual(
+ resizedImage, originalImage, "Should return the original image when \(MaxSize.self) is nil.")
+ }
+
+ func testResizeExceedingMaxSize() {
+ let originalImage = createTestImage(size: NSSize(width: 1200, height: 800))
+
+ let maxSize = MaxSize(width: 600, height: 400)
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize)
+
+ // The resized image should be scaled down to fit within the max size while maintaining the aspect ratio.
+ XCTAssertEqual(
+ resizedImage.size.width, maxSize.width,
+ "Resized image width should not exceed the maximum allowed width.")
+ XCTAssertEqual(
+ resizedImage.size.height, maxSize.height,
+ "Resized image height should not exceed the maximum allowed height.")
+ }
+
+ func testResizeBelowMaxSize() {
+ let originalImage = createTestImage(size: NSSize(width: 600, height: 400))
+
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: MaxSize(width: 1200, height: 800))
+
+ // The resized image should remain the same size, as it's already smaller than the max size.
+ XCTAssertEqual(
+ resizedImage.size.width, originalImage.size.width,
+ "Resized image width should remain unchanged when smaller than the maximum allowed width.")
+ XCTAssertEqual(
+ resizedImage.size.height, originalImage.size.height,
+ "Resized image height should remain unchanged when smaller than the maximum allowed height.")
+ }
+
+ func testResizeWidthOnly() {
+ // An image where only the width exceeds the max size
+ let originalImage = createTestImage(size: NSSize(width: 600, height: 200))
+
+ let maxSize = MaxSize(width: 300, height: 400)
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize)
+
+ // The image should be resized proportionally based on width
+ XCTAssertEqual(
+ resizedImage.size.width, maxSize.width, "The width should be equal to max width.")
+ XCTAssertEqual(resizedImage.size.height, 100, "The height should be resized proportionally.")
+ }
+
+ func testResizeHeightOnly() {
+ // An image where only the height exceeds the max size
+ let originalImage = createTestImage(size: NSSize(width: 400, height: 600))
+
+ let maxSize = MaxSize(width: 500, height: 300)
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize)
+
+ // The image should be resized proportionally based on height
+ XCTAssertEqual(resizedImage.size.width, 200, "The width should be resized proportionally.")
+ XCTAssertEqual(
+ resizedImage.size.height, maxSize.height, "The height should be equal to max height.")
+ }
+
+ func testResizeExtremeAspectRatio() {
+ // An image (20:1) with an extreme aspect ratio (very wide)
+ let originalImage = createTestImage(size: NSSize(width: 2000, height: 100))
+
+ let maxSize = MaxSize(width: 600, height: 400)
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize)
+
+ // The resized image should be within the max size while maintaining aspect ratio
+ XCTAssertEqual(resizedImage.size.width, 600, "The width should be resized to max width")
+ XCTAssertEqual(resizedImage.size.height, 30, "The height should be resized proportionally.")
+ }
+
+ func testResizeImageWithSameAspectRatio() {
+ let originalImage = createTestImage(size: NSSize(width: 800, height: 400))
+
+ let maxSize = MaxSize(width: 600, height: 300)
+ let resizedImage = originalImage.resizedOrOriginal(maxSize: maxSize)
+
+ XCTAssertEqual(
+ resizedImage.size.width, maxSize.width,
+ "Width should be equal to max width when the aspect ratio is the same")
+ XCTAssertEqual(
+ resizedImage.size.height, maxSize.height,
+ "Height should be equal to height width when the aspect ratio is the same")
+ }
+
+ func testResizedOrOriginalWithUndefinedSize() {
+ let image = createTestImage(size: NSSize(width: 300, height: 200))
+ let resizedImage = image.resizedOrOriginal(maxSize: MaxSize())
+
+ XCTAssertEqual(
+ image.size.width, resizedImage.size.width,
+ "Should return the original image without resizing.")
+ XCTAssertEqual(
+ image.size.height, resizedImage.size.height,
+ "Should return the original image without resizing.")
+ }
+
+ func testShouldResize() {
+ let imageSize = NSSize(width: 400, height: 600)
+ let image = NSImage(size: imageSize)
+
+ XCTAssertFalse(
+ image.shouldResize(maxSize: MaxSize()),
+ "Should not resize when both the width and height are nil."
+ )
+
+ XCTAssertTrue(
+ image.shouldResize(maxSize: MaxSize(width: 300, height: 500)),
+ "Should resize when image size larger than max size."
+ )
+ XCTAssertTrue(
+ image.shouldResize(maxSize: MaxSize(width: 300)),
+ "Should resize when image width larger than max width."
+ )
+ XCTAssertTrue(
+ image.shouldResize(maxSize: MaxSize(height: 500)),
+ "Should resize when image height larger than max height."
+ )
+
+ XCTAssertFalse(
+ image.shouldResize(maxSize: MaxSize(width: 500, height: 700)),
+ "Should not resize when image size smaller than max size."
+ )
+ XCTAssertFalse(
+ image.shouldResize(maxSize: MaxSize(width: 500)),
+ "Should not resize when image width smaller than max width."
+ )
+ XCTAssertFalse(
+ image.shouldResize(maxSize: MaxSize(height: 700)),
+ "Should not resize when image height smaller than max height."
+ )
+
+ XCTAssertFalse(
+ image.shouldResize(maxSize: MaxSize(width: imageSize.width, height: imageSize.height)),
+ "Should not resize when image size equal max size."
+ )
+
+ XCTAssertTrue(
+ image.shouldResize(maxSize: MaxSize(width: 350, height: 700)),
+ "Should resize when image width larger than max width and image height less than max height."
+ )
+ XCTAssertTrue(
+ image.shouldResize(maxSize: MaxSize(width: 450, height: 500)),
+ "Should resize when image height is larger than max height and image width less than max width"
+ )
+
+ }
+
+ func testHasAnyDimension() {
+ XCTAssertFalse(
+ MaxSize(width: nil, height: nil).hasAnyDimension(),
+ "Should not resize when both width and height are nil.")
+ XCTAssertTrue(
+ MaxSize(width: 20, height: nil).hasAnyDimension(),
+ "Should resize when width is specified and height is nil.")
+ XCTAssertTrue(
+ MaxSize(width: nil, height: 20).hasAnyDimension(),
+ "Should resize when height is specified and width is nil.")
+ XCTAssertTrue(
+ MaxSize(width: 20, height: 20).hasAnyDimension(),
+ "Should resize when both width and height are specified.")
+ }
+
+ func testMaxSizeToNSSize_withDefinedWidthAndHeight() {
+ let image = createTestImage(size: NSSize(width: 50, height: 50))
+ let maxSize = MaxSize(width: 32, height: 96)
+
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).width, maxSize.width,
+ "Expected width to match MaxSize width.")
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).height, maxSize.height,
+ "Expected height to match MaxSize height.")
+ }
+
+ func testMaxSizeToNSSize_withDefinedWidthOnly() {
+ let image = createTestImage(size: NSSize(width: 50, height: 50))
+ let maxSize = MaxSize(width: 128)
+
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).width, maxSize.width,
+ "Expected width to match MaxSize width.")
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).height, image.size.height,
+ "Expected height to default to image height when MaxSize height is nil.")
+ }
+
+ func testMaxSizeToNSSize_withDefinedHeightOnly() {
+ let image = createTestImage(size: NSSize(width: 50, height: 50))
+ let maxSize = MaxSize(height: 64)
+
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).width, image.size.width,
+ "Expected width to default to image width when MaxSize width is nil.")
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).height, maxSize.height,
+ "Expected height to match MaxSize height.")
+ }
+
+ func testMaxSizeToNSSize_withUndefinedWidthAndHeight() {
+ let image = createTestImage(size: NSSize(width: 50, height: 50))
+ let maxSize = MaxSize()
+
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).width, image.size.width,
+ "Expected width to default to image width when MaxSize width is nil.")
+ XCTAssertEqual(
+ maxSize.toNSSize(image: image).height, image.size.height,
+ "Expected height to default to image height when MaxSize height is nil.")
+ }
+
+}
diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift
new file mode 100644
index 000000000000..6b6becdf3241
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/example/macos/RunnerTests/RunnerTests.swift
@@ -0,0 +1,94 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import XCTest
+
+@testable import image_picker_macos
+
+final class RunnerTests: XCTestCase {
+
+ func testSupportsPHPicker() {
+ let imagePicker = ImagePickerImpl()
+ if #available(macOS 13.0, *) {
+ XCTAssertTrue(
+ imagePicker.supportsPHPicker(),
+ "PHPicker is expected to be supported on macOS 13.0 and newer versions.")
+ } else {
+ XCTAssertFalse(
+ imagePicker.supportsPHPicker(),
+ "PHPicker is expected to be unsupported on macOS versions older than 13.0.")
+ }
+ }
+
+ func testImageFileType() {
+ XCTAssertEqual(
+ imageFileType(quality: 100), NSBitmapImageRep.FileType.png,
+ "Quality 100 should return PNG file type.")
+ XCTAssertEqual(
+ imageFileType(quality: 99), NSBitmapImageRep.FileType.jpeg,
+ "Quality below 100 should return JPEG file type.")
+ XCTAssertEqual(
+ imageFileType(quality: nil), NSBitmapImageRep.FileType.png,
+ "Quality nil should return PNG file type.")
+ }
+
+ func testImageFileExt() {
+ XCTAssertEqual(
+ imageFileExt(fileType: NSBitmapImageRep.FileType.png), "png",
+ "File extension for PNG should be 'png'.")
+ XCTAssertEqual(
+ imageFileExt(fileType: NSBitmapImageRep.FileType.jpeg), "jpeg",
+ "File extension for JPEG should be 'jpeg'.")
+ }
+
+ func testGenerateUniqueImageFileName() {
+ let fileType = NSBitmapImageRep.FileType.jpeg
+ let generatedFileName = generateUniqueImageFileName(imageFileType: fileType)
+ let expectedExtension = imageFileExt(fileType: fileType)
+
+ // Extract the UUID part of the generated file name
+ let uuidStringFromFile = generatedFileName.replacingOccurrences(
+ of: ".\(expectedExtension)", with: "")
+
+ let fileUUID = UUID(uuidString: uuidStringFromFile)
+
+ XCTAssertNotNil(fileUUID, "Generated file name should start with a valid UUID.")
+ XCTAssertTrue(
+ generatedFileName.hasSuffix(".\(expectedExtension)"),
+ "Generated file name should have a '\(expectedExtension)' extension.")
+ }
+
+ func testGenerateTempImageFilePath() {
+ let fileType: NSBitmapImageRep.FileType = NSBitmapImageRep.FileType.png
+ let filePath = generateTempImageFilePath(imageFileType: fileType)
+ let fileExists = FileManager.default.fileExists(atPath: filePath.path)
+
+ XCTAssertFalse(fileExists, "The file at path \(filePath) should not exist.")
+ XCTAssertEqual(filePath.pathExtension, "png", "The file path should have a .png extension.")
+ XCTAssertTrue(
+ filePath.absoluteString.hasPrefix(FileManager.default.temporaryDirectory.absoluteString),
+ "The file path should be in the temporary directory.")
+
+ XCTAssertTrue(filePath.isFileURL, "The generated path should be a file URL.")
+
+ let anotherFilePath = generateTempImageFilePath(imageFileType: fileType)
+ XCTAssertNotEqual(filePath, anotherFilePath, "The generated file paths should be unique.")
+ }
+
+ func testPathString() {
+ let tempDirectory = FileManager.default.temporaryDirectory
+ let fileURL = tempDirectory.appendingPathComponent("flutter.dart")
+
+ XCTAssertEqual(
+ fileURL.path, fileURL.pathString(),
+ "Expected pathString() to match `URL.path` for the current URL.")
+
+ if #available(macOS 13.0, *) {
+ XCTAssertEqual(
+ fileURL.path(), fileURL.pathString(),
+ "Expected pathString() to match `URL.path()` for macOS 13.0 and later.")
+ }
+ }
+
+}
diff --git a/packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift b/packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift
new file mode 100644
index 000000000000..df50b4c08125
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/example/macos/RunnerUITests/RunnerUITests.swift
@@ -0,0 +1,32 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import FlutterMacOS
+import XCTest
+
+@testable import image_picker_macos
+
+/// The specified amount of time for waiting to check if an element exists.
+let kElementWaitingTime: TimeInterval = 30
+
+final class RunnerUITests: XCTestCase {
+
+ var app: XCUIApplication!
+
+ override func setUp() {
+ continueAfterFailure = false
+ app = XCUIApplication()
+ app.launch()
+ }
+
+ override func tearDown() {
+ app.terminate()
+ }
+
+ @MainActor
+ func testImagePicker() throws {
+ // TODO(EchoEllet): Lacks native UI tests https://discord.com/channels/608014603317936148/1300517990957056080/1300518056690188361
+ // https://github.com/flutter/flutter/issues/70234
+ }
+}
diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml
index ab0d82bbc305..bdbd284f3c7f 100644
--- a/packages/image_picker/image_picker_macos/example/pubspec.yaml
+++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml
@@ -24,6 +24,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
+ integration_test:
+ sdk: flutter
flutter:
uses-material-design: true
diff --git a/packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart
new file mode 100644
index 000000000000..4f10f2a522f3
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/example/test_driver/integration_test.dart
@@ -0,0 +1,7 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:integration_test/integration_test_driver.dart';
+
+Future main() => integrationDriver();
diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart
index 9e9447a5710c..d509d019a3c1 100644
--- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart
+++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart
@@ -7,6 +7,8 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac
import 'package:flutter/foundation.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+import 'src/messages.g.dart';
+
/// The macOS implementation of [ImagePickerPlatform].
///
/// This class implements the `package:image_picker` functionality for
@@ -15,6 +17,10 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
/// Constructs a platform implementation.
ImagePickerMacOS();
+ /// The platform API generated by Pigeon to communicate with native macOS using a method channel.
+ /// Used only when [useMacOSPHPicker] is `true` for **PHPicker implementation**.
+ final ImagePickerApi _hostApi = ImagePickerApi();
+
/// The file selector used to prompt the user to select images or videos.
@visibleForTesting
static FileSelectorPlatform fileSelector = FileSelectorMacOS();
@@ -24,6 +30,42 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
ImagePickerPlatform.instance = ImagePickerMacOS();
}
+ /// Sets [ImagePickerMacOS] to use [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller)
+ /// which is **only supported on macOS 13.0+**.
+ ///
+ /// Will fallback to [file_selector_macos](https://pub.dev/packages/file_selector_macos)
+ /// if [useMacOSPHPicker] is `false` or the macOS version doesn't support
+ /// this feature.
+ ///
+ /// Currently defaults to `false`.
+ ///
+ /// **Note**: This implementation depends on the photos in the [Photos for macOS App](https://www.apple.com/in/macos/photos/),
+ /// if the user didn't open the app or import any photos to the app,
+ /// they will see: `No photos` or `No Photos or Videos` message even if they
+ /// have them as files on their desktop.
+ ///
+ /// Supports picking an image, multi-image, video, media, and multiple media.
+ bool useMacOSPHPicker = false;
+
+ // TODO(EchoEllet): shouldUsePHPicker() and supportsPHPicker() should not be public, avoid using @visibleForTesting
+
+ /// Return `true` if the current macOS version supports [useMacOSPHPicker].
+ ///
+ /// The [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller)
+ /// is **supported on macOS 13.0+**
+ @visibleForTesting
+ Future supportsPHPicker() => _hostApi.supportsPHPicker();
+
+ /// Returns `true` if [ImagePickerMacOS] should use [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller).
+ ///
+ /// See also:
+ ///
+ /// * [useMacOSPHPicker] to check whether **PHPicker** should be preferred over [file_selector_macos](https://pub.dev/packages/file_selector_macos).
+ /// * [supportsPHPicker] to verify if the current macOS version supports **PHPicker**.
+ @visibleForTesting
+ Future shouldUsePHPicker() async =>
+ await supportsPHPicker() && useMacOSPHPicker;
+
// This is soft-deprecated in the platform interface, and is only implemented
// for compatibility. Callers should be using getImageFromSource.
@override
@@ -83,8 +125,9 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
preferredCameraDevice: preferredCameraDevice));
}
- // [ImagePickerOptions] options are not currently supported. If any
- // of its fields are set, they will be silently ignored.
+ // [ImagePickerOptions] options are currently only supported when using
+ // PHPicker implementation. If any of its fields are set,
+ // they will be silently ignored.
//
// If source is `ImageSource.camera`, a `StateError` will be thrown
// unless a [cameraDelegate] is set.
@@ -97,9 +140,18 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
case ImageSource.camera:
return super.getImageFromSource(source: source);
case ImageSource.gallery:
- // TODO(stuartmorgan): Add a native implementation that can use
- // PHPickerViewController on macOS 13+, with this as a fallback for
- // older OS versions: https://github.com/flutter/flutter/issues/125829.
+ if (await shouldUsePHPicker()) {
+ final String? imagePath = (await _hostApi.pickImages(
+ _imageOptionsToImageSelectionOptions(options),
+ GeneralOptions(limit: 1),
+ ))
+ .firstOrNull;
+ if (imagePath == null) {
+ return null;
+ }
+
+ return XFile(imagePath);
+ }
const XTypeGroup typeGroup =
XTypeGroup(uniformTypeIdentifiers: ['public.image']);
final XFile? file = await fileSelector
@@ -130,6 +182,15 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
preferredCameraDevice: preferredCameraDevice,
maxDuration: maxDuration);
case ImageSource.gallery:
+ if (await shouldUsePHPicker()) {
+ final String? videoPath =
+ (await _hostApi.pickVideos(GeneralOptions(limit: 1))).firstOrNull;
+ if (videoPath == null) {
+ return null;
+ }
+
+ return XFile(videoPath);
+ }
const XTypeGroup typeGroup =
XTypeGroup(uniformTypeIdentifiers: ['public.movie']);
final XFile? file = await fileSelector
@@ -141,18 +202,41 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
throw UnimplementedError('Unknown ImageSource: $source');
}
- // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently
- // supported. If any of these arguments are supplied, they will be silently
- // ignored.
+ // This is soft-deprecated in the platform interface, and is only implemented
+ // for compatibility. Callers should be using getMultiImageWithOptions.
@override
Future> getMultiImage({
double? maxWidth,
double? maxHeight,
int? imageQuality,
}) async {
- // TODO(stuartmorgan): Add a native implementation that can use
- // PHPickerViewController on macOS 13+, with this as a fallback for
- // older OS versions: https://github.com/flutter/flutter/issues/125829.
+ return getMultiImageWithOptions(
+ options: MultiImagePickerOptions(
+ imageOptions: ImageOptions(
+ imageQuality: imageQuality,
+ maxHeight: maxHeight,
+ maxWidth: maxWidth,
+ ),
+ ),
+ );
+ }
+
+ // [MultiImagePickerOptions] options are currently only
+ // supported when using PHPicker implementation. If any of these arguments are supplied, they will be silently
+ // ignored.
+ @override
+ Future> getMultiImageWithOptions({
+ MultiImagePickerOptions options = const MultiImagePickerOptions(),
+ }) async {
+ if (await shouldUsePHPicker()) {
+ final List images = await _hostApi.pickImages(
+ _imageOptionsToImageSelectionOptions(options.imageOptions),
+ GeneralOptions(
+ limit: options.limit ?? 0,
+ ),
+ );
+ return images.map((String imagePath) => XFile(imagePath)).toList();
+ }
const XTypeGroup typeGroup =
XTypeGroup(uniformTypeIdentifiers: ['public.image']);
final List files = await fileSelector
@@ -160,11 +244,35 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
return files;
}
- // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently
- // supported. If any of these arguments are supplied, they will be silently
+ ImageSelectionOptions _imageOptionsToImageSelectionOptions(
+ ImageOptions imageOptions,
+ ) {
+ return ImageSelectionOptions(
+ quality: imageOptions.imageQuality ?? 100,
+ maxSize: MaxSize(
+ width: imageOptions.maxWidth,
+ height: imageOptions.maxHeight,
+ ),
+ );
+ }
+
+ // [ImageOptions] options are currently only
+ // supported when using PHPicker implementation. If any of these arguments are supplied, they will be silently
// ignored.
@override
Future> getMedia({required MediaOptions options}) async {
+ if (await shouldUsePHPicker()) {
+ final List images = await _hostApi.pickMedia(
+ MediaSelectionOptions(
+ imageSelectionOptions:
+ _imageOptionsToImageSelectionOptions(options.imageOptions),
+ ),
+ GeneralOptions(
+ limit: options.limit ?? (options.allowMultiple ? 0 : 1),
+ ),
+ );
+ return images.map((String mediaPath) => XFile(mediaPath)).toList();
+ }
const XTypeGroup typeGroup = XTypeGroup(
label: 'images and videos',
extensions: ['public.image', 'public.movie']);
diff --git a/packages/image_picker/image_picker_macos/lib/src/messages.g.dart b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart
new file mode 100644
index 000000000000..19d2bf53a063
--- /dev/null
+++ b/packages/image_picker/image_picker_macos/lib/src/messages.g.dart
@@ -0,0 +1,297 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Autogenerated from Pigeon (v22.6.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
+
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+PlatformException _createConnectionError(String channelName) {
+ return PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel: "$channelName".',
+ );
+}
+
+List