Skip to content

Latest commit

 

History

History
281 lines (190 loc) · 15.8 KB

README.md

File metadata and controls

281 lines (190 loc) · 15.8 KB

🖼️ Native Image Picker for macOS

Pub Version Star on Github License: MIT Dart Code Coverage Tests

A macOS platform implementation of image_picker using the native system picker instead of the system open file dialog.

This package is an alternative to image_picker_macos which uses file_selector.

Note

This native picker depends on the photos in the Photos for MacOS App, which uses the Apple PhotosUI Picker, also known as PHPicker.

Default picker Native picker
Default picker macOS PHPicker

✨ Features

  • 🚀 Seamless Integration
    Effortlessly integrates with the image_picker package. Switch seamlessly between image_picker_macos and this native platform implementation without modifying existing code.
  • 🔒 No Permissions or Setup Required
    Requires no runtime permission prompts or entitlement configuration. Everything works out of the box.
  • 📱 macOS Photos App
    Enables picking images from the macOS Photos app, integrating with the Apple ecosystem and supporting photo imports from connected iOS devices.
  • 🛠️ Supports Image Options
    Adds support for image arguments like maxWidth, maxHeight, and imageQuality—features not currently supported in image_picker_macos.

Import photos from the connected iOS devices to macOS

🛠️ Getting started

Run the following command to add the dependencies:

$ flutter pub add image_picker image_picker_macos native_image_picker_macos
  1. image_picker: The app-facing package for the Image Picker plugin which specifies the API used by Flutter apps.
  2. image_picker_macos: The default macOS implementation of image_picker, built on file_selector and using NSOpenPanel with appropriate file type filters set. Lacks image resizing/compression options but supports older macOS versions.
  3. native_image_picker_macos: A macOS implementation of image_picker, built on PHPickerViewController which depends on the Photos for macOS App. Supports image resizing/compression options. Requires macOS 13.0+.

Using both image_picker_macos and native_image_picker_macos can enable user-level opt-in to switch between implementations if the user prefers to pick images from the photos app or the file system.

Use native macOS picker switch button

The platform implementation image_picker_macos is required to ensure compatibility with macOS versions before 13.0, which is used as a fallback, in that case, it's necessary to setup image_picker_macos.

Tip

After registering this implementation as outlined in the Usage section, you can use the image_picker plugin as usual.

📜 Usage

By default, this package doesn't replace the default implementation of image_picker for macOS to avoid conflict with image_picker_macos.

This implementation supports macOS 13 Ventura and later.

To apply this package only in case it's supported on the current macOS version:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NativeImagePickerMacOS.registerWithIfSupported(); // ADD THIS LINE

  runApp(const MainApp());
}

To checks if the current implementation of image_picker is native_image_picker_macos:

final bool isRegistered = NativeImagePickerMacOS.isRegistered();

// OR

import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';

final bool isRegistered = ImagePickerPlatform.instance is NativeImagePickerMacOS;

To checks if the current macOS version supports this implementation:

final bool isSupported = await NativeImagePickerMacOS.isSupported(); // Returns false on non-macOS platforms or if PHPicker is not supported on the current macOS version.

To switch between image_picker_macos and native_image_picker_macos implementations:

// NOTE: This code assumes the current target platform is macOS and native_image_picker_macos implementation is supported.

import 'package:image_picker_macos/image_picker_macos.dart';
import 'package:native_image_picker_macos/native_image_picker_macos.dart';

// To switch to image_picker_macos (supported on all macOS versions):

ImagePickerMacOS.registerWith();

// To switch to native_image_picker_macos (supported on macOS 13 and above):

NativeImagePickerMacOS.registerWith();

To open the macOS photos app:

await NativeImagePickerMacOS.instanceOrNull?.openPhotosApp();

// OR

final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
if (imagePickerImplementation is NativeImagePickerMacOS) {
  await imagePickerImplementation.openPhotosApp();
}

Tip

You can use NativeImagePickerMacOS.registerWith() to register this implementation. However, this bypasses platform checks, which may result in runtime errors if the current platform is not macOS or if the macOS version is unsupported. Instead, use registerWithIfSupported() if uncertain.

Refer to the example main.dart for a full usage example.

🌱 Contributing

This package uses pigeon for platform communication with the platform host and mockito for mocking in unit tests and swift-format for formatting the Swift code.

$ dart run pigeon --input pigeons/messages.dart # To generate the required Dart and host-language code.
$ dart run build_runner build --delete-conflicting-outputs # To generate the mock classes.
$ swift-format format --in-place --recursive macos/native_image_picker_macos/Sources/native_image_picker_macos example/macos/Runner example/macos/RunnerTests example/macos/RunnerUITests # To format the Swift code.
$ dart format . # To format the Dart code.
$ (cd example/macos && xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug -quiet) # To run the native macOS unit tests.
$ flutter test # To run the Flutter unit tests.

Resources

Contributions are welcome. File issues to the GitHub repo.

ℹ️ Limitations

📚 Additional information

This functionality was originally proposed as a pull request to image_picker_macos, but it was later decided to split it into a community package which is unendorsed.

PHPicker window

Ask a question about using the package.

🧪 Testing

Tip

With this approach, you can effectively test this platform implementation with the existing packages that use image_picker APIs. All platform-specific calls to NativeImagePickerMacOS should use the instance from ImagePickerPlatform.instance instead of creating a new NativeImagePickerMacOS to work.

To override the methods implementation for unit testing, add the dev dependencies:

  1. mockito (or mocktail): for mocking the instance methods of NativeImagePickerMacOS.
  2. image_picker_platform_interface: for overriding the instance of ImagePickerPlatform with the mock instance.
  3. build_runner: for creating the generated Dart files.
  4. plugin_platform_interface: Since ImagePickerPlatform extends PlatformInterface, it's required to apply the mixin MockPlatformInterfaceMixin to the mock of NativeImagePickerMacOS to ignore an assertation failure that enforces the usage of extends instead of implements, since mock classes need to extend Mock and implement the real class.
$ flutter pub add dev:mockito dev:image_picker_platform_interface dev:build_runner dev:plugin_platform_interface # Add them as dev-dependencies

In your test file, add this annotation somewhere:

import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:native_image_picker_macos/native_image_picker_macos.dart';

@GenerateNiceMocks([MockSpec<NativeImagePickerMacOS>()])

Generate the MockNativeImagePickerMacOS by running:

$ dart run build_runner build --delete-conflicting-outputs

Create a new instance of MockNativeImagePickerMacOS and override the instance of ImagePickerPlatform to every test:

import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late MockNativeImagePickerMacOS mockNativeImagePickerMacOS;

  setUp(() {
    mockNativeImagePickerMacOS = MockNativeImagePickerMacOS();

    ImagePickerPlatform.instance = mockNativeImagePickerMacOS;
  });

  // Your tests, example:

  testWidgets(
    'pressing the open photos button calls openPhotosApp from $NativeImagePickerMacOS',
    (WidgetTester tester) async {
      await tester
          .pumpWidget(const ExampleWidget()); // REPLACE WITH THE TARGET WIDGET

      final openPhotosFinder =
          find.text('Open Photos App'); // REPLACE WITH THE BUTTON TEXT

      expect(openPhotosFinder, findsOneWidget);

      // Assuming the openPhotosApp call will success.
      when(mockNativeImagePickerMacOS.openPhotosApp())
          .thenAnswer((_) async => true);

      await tester.tap(openPhotosFinder);
      await tester.pump();

      verify(mockNativeImagePickerMacOS.openPhotosApp()).called(1);
      verifyNoMoreInteractions(mockNativeImagePickerMacOS);
    },
  );

  // ...
}

However, if you run the tests, you will get the following error:

 Assertion failed: "Platform interfaces must not be implemented with `implements`"

And that is because by default, all plugin platform interfaces that inherit from PlatformInterface must extends and not implements it to avoid breaking changes (adding new methods to platform interfaces are not considered breaking changes).

And mock classes must implements the real class rather than extends them, a solution is to provide the mixin MockPlatformInterfaceMixin from plugin_platform_interface that will override this check:

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

// This doesn't work yet since MockNativeImagePickerMacOS is generated, unlike the mocktail package.
class MockNativeImagePickerMacOS extends Mock
    with MockPlatformInterfaceMixin
    implements NativeImagePickerMacOS {}

And since MockNativeImagePickerMacOS is generated, we need a new class that extends the base mock and provides the MockPlatformInterfaceMixin for plugin_platform_interface to not throw the assertion failure:

@GenerateNiceMocks([MockSpec<NativeImagePickerMacOS>(as: Symbol('BaseMockNativeImagePickerMacOS'))]) // This name should be different than MockNativeImagePickerMacOS for the mockito generation to success
import '<current-test-file-name>.mocks.dart'; // REPLACE <current-test-file-name> with the current test file name without extension
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

class MockNativeImagePickerMacOS extends BaseMockNativeImagePickerMacOS
    with MockPlatformInterfaceMixin {}

// Use MockNativeImagePickerMacOS instead of BaseMockNativeImagePickerMacOS for creating the mock of NativeImagePickerMacOS

Refer to the example main_test.dart for the full example test.