Skip to content

Commit

Permalink
[ios_platform_images] Convert to Pigeon (#4945)
Browse files Browse the repository at this point in the history
Replaces the manual platform method code with Pigeon.

Also adds some additional Dart unit tests. Since the needs are minimal, this uses a manual fake rather than introducing Mockito.

Fixes #117911
  • Loading branch information
stuartmorgan authored Sep 19, 2023
1 parent d08ebfd commit 51e74b9
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 65 deletions.
4 changes: 4 additions & 0 deletions packages/ios_platform_images/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.2+3

* Converts platform communication to Pigeon.

## 0.2.2+2

* Adds pub topics to package metadata.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#import <Flutter/Flutter.h>

#import "messages.g.h"

/// A plugin for Flutter that allows Flutter to load images in a platform
/// specific way on iOS.
@interface IosPlatformImagesPlugin : NSObject <FlutterPlugin>
@interface IosPlatformImagesPlugin : NSObject <FlutterPlugin, FPIPlatformImagesApi>
@end
50 changes: 21 additions & 29 deletions packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,27 @@ @interface IosPlatformImagesPlugin ()
@implementation IosPlatformImagesPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/ios_platform_images"
binaryMessenger:[registrar messenger]];

[channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
if ([@"loadImage" isEqualToString:call.method]) {
NSString *name = call.arguments;
UIImage *image = [UIImage imageNamed:name];
NSData *data = UIImagePNGRepresentation(image);
if (data) {
result(@{
@"scale" : @(image.scale),
@"data" : [FlutterStandardTypedData typedDataWithBytes:data],
});
} else {
result(nil);
}
return;
} else if ([@"resolveURL" isEqualToString:call.method]) {
NSArray *args = call.arguments;
NSString *name = args[0];
NSString *extension = (args[1] == (id)NSNull.null) ? nil : args[1];

NSURL *url = [[NSBundle mainBundle] URLForResource:name withExtension:extension];
result(url.absoluteString);
return;
}
result(FlutterMethodNotImplemented);
}];
FPIPlatformImagesApiSetup(registrar.messenger, [[IosPlatformImagesPlugin alloc] init]);
}

- (nullable FPIPlatformImageData *)
loadImageWithName:(nonnull NSString *)name
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
UIImage *image = [UIImage imageNamed:name];
NSData *data = UIImagePNGRepresentation(image);
if (!data) {
return nil;
}
return [FPIPlatformImageData makeWithData:[FlutterStandardTypedData typedDataWithBytes:data]
scale:@(image.scale)];
}

- (nullable NSString *)resolveURLForResource:(nonnull NSString *)name
withExtension:(nullable NSString *)extension
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)
error {
NSURL *url = [[NSBundle mainBundle] URLForResource:name withExtension:extension];
return url.absoluteString;
}

@end
47 changes: 47 additions & 0 deletions packages/ios_platform_images/ios/Classes/messages.g.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon

#import <Foundation/Foundation.h>

@protocol FlutterBinaryMessenger;
@protocol FlutterMessageCodec;
@class FlutterError;
@class FlutterStandardTypedData;

NS_ASSUME_NONNULL_BEGIN

@class FPIPlatformImageData;

/// A serialization of a platform image's data.
@interface FPIPlatformImageData : NSObject
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)makeWithData:(FlutterStandardTypedData *)data scale:(NSNumber *)scale;
/// The image data.
@property(nonatomic, strong) FlutterStandardTypedData *data;
/// The image's scale factor.
@property(nonatomic, strong) NSNumber *scale;
@end

/// The codec used by FPIPlatformImagesApi.
NSObject<FlutterMessageCodec> *FPIPlatformImagesApiGetCodec(void);

@protocol FPIPlatformImagesApi
/// Returns the URL for the given resource, or null if no such resource is
/// found.
- (nullable NSString *)resolveURLForResource:(NSString *)resourceName
withExtension:(nullable NSString *)extension
error:(FlutterError *_Nullable *_Nonnull)error;
/// Returns the data for the image resource with the given name, or null if
/// no such resource is found.
- (nullable FPIPlatformImageData *)loadImageWithName:(NSString *)name
error:(FlutterError *_Nullable *_Nonnull)error;
@end

extern void FPIPlatformImagesApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<FPIPlatformImagesApi> *_Nullable api);

NS_ASSUME_NONNULL_END
163 changes: 163 additions & 0 deletions packages/ios_platform_images/ios/Classes/messages.g.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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 (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon

#import "messages.g.h"

#if TARGET_OS_OSX
#import <FlutterMacOS/FlutterMacOS.h>
#else
#import <Flutter/Flutter.h>
#endif

#if !__has_feature(objc_arc)
#error File requires ARC to be enabled.
#endif

static NSArray *wrapResult(id result, FlutterError *error) {
if (error) {
return @[
error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null]
];
}
return @[ result ?: [NSNull null] ];
}
static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
id result = array[key];
return (result == [NSNull null]) ? nil : result;
}

@interface FPIPlatformImageData ()
+ (FPIPlatformImageData *)fromList:(NSArray *)list;
+ (nullable FPIPlatformImageData *)nullableFromList:(NSArray *)list;
- (NSArray *)toList;
@end

@implementation FPIPlatformImageData
+ (instancetype)makeWithData:(FlutterStandardTypedData *)data scale:(NSNumber *)scale {
FPIPlatformImageData *pigeonResult = [[FPIPlatformImageData alloc] init];
pigeonResult.data = data;
pigeonResult.scale = scale;
return pigeonResult;
}
+ (FPIPlatformImageData *)fromList:(NSArray *)list {
FPIPlatformImageData *pigeonResult = [[FPIPlatformImageData alloc] init];
pigeonResult.data = GetNullableObjectAtIndex(list, 0);
NSAssert(pigeonResult.data != nil, @"");
pigeonResult.scale = GetNullableObjectAtIndex(list, 1);
NSAssert(pigeonResult.scale != nil, @"");
return pigeonResult;
}
+ (nullable FPIPlatformImageData *)nullableFromList:(NSArray *)list {
return (list) ? [FPIPlatformImageData fromList:list] : nil;
}
- (NSArray *)toList {
return @[
(self.data ?: [NSNull null]),
(self.scale ?: [NSNull null]),
];
}
@end

@interface FPIPlatformImagesApiCodecReader : FlutterStandardReader
@end
@implementation FPIPlatformImagesApiCodecReader
- (nullable id)readValueOfType:(UInt8)type {
switch (type) {
case 128:
return [FPIPlatformImageData fromList:[self readValue]];
default:
return [super readValueOfType:type];
}
}
@end

@interface FPIPlatformImagesApiCodecWriter : FlutterStandardWriter
@end
@implementation FPIPlatformImagesApiCodecWriter
- (void)writeValue:(id)value {
if ([value isKindOfClass:[FPIPlatformImageData class]]) {
[self writeByte:128];
[self writeValue:[value toList]];
} else {
[super writeValue:value];
}
}
@end

@interface FPIPlatformImagesApiCodecReaderWriter : FlutterStandardReaderWriter
@end
@implementation FPIPlatformImagesApiCodecReaderWriter
- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
return [[FPIPlatformImagesApiCodecWriter alloc] initWithData:data];
}
- (FlutterStandardReader *)readerWithData:(NSData *)data {
return [[FPIPlatformImagesApiCodecReader alloc] initWithData:data];
}
@end

NSObject<FlutterMessageCodec> *FPIPlatformImagesApiGetCodec(void) {
static FlutterStandardMessageCodec *sSharedObject = nil;
static dispatch_once_t sPred = 0;
dispatch_once(&sPred, ^{
FPIPlatformImagesApiCodecReaderWriter *readerWriter =
[[FPIPlatformImagesApiCodecReaderWriter alloc] init];
sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
});
return sSharedObject;
}

void FPIPlatformImagesApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
NSObject<FPIPlatformImagesApi> *api) {
/// Returns the URL for the given resource, or null if no such resource is
/// found.
{
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
initWithName:@"dev.flutter.pigeon.ios_platform_images.PlatformImagesApi.resolveUrl"
binaryMessenger:binaryMessenger
codec:FPIPlatformImagesApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(resolveURLForResource:withExtension:error:)],
@"FPIPlatformImagesApi api (%@) doesn't respond to "
@"@selector(resolveURLForResource:withExtension:error:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSString *arg_resourceName = GetNullableObjectAtIndex(args, 0);
NSString *arg_extension = GetNullableObjectAtIndex(args, 1);
FlutterError *error;
NSString *output = [api resolveURLForResource:arg_resourceName
withExtension:arg_extension
error:&error];
callback(wrapResult(output, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
/// Returns the data for the image resource with the given name, or null if
/// no such resource is found.
{
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
initWithName:@"dev.flutter.pigeon.ios_platform_images.PlatformImagesApi.loadImage"
binaryMessenger:binaryMessenger
codec:FPIPlatformImagesApiGetCodec()];
if (api) {
NSCAssert(
[api respondsToSelector:@selector(loadImageWithName:error:)],
@"FPIPlatformImagesApi api (%@) doesn't respond to @selector(loadImageWithName:error:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSString *arg_name = GetNullableObjectAtIndex(args, 0);
FlutterError *error;
FPIPlatformImageData *output = [api loadImageWithName:arg_name error:&error];
callback(wrapResult(output, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
}
37 changes: 25 additions & 12 deletions packages/ios_platform_images/lib/ios_platform_images.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart'
show SynchronousFuture, describeIdentity, immutable, objectRuntimeType;
show
SynchronousFuture,
describeIdentity,
immutable,
objectRuntimeType,
visibleForTesting;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

import 'src/messages.g.dart';

class _FutureImageStreamCompleter extends ImageStreamCompleter {
_FutureImageStreamCompleter({
required Future<ui.Codec> codec,
Expand Down Expand Up @@ -101,27 +108,34 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> {
'(${describeIdentity(_futureBytes)}, scale: $_futureScale)';
}

PlatformImagesApi _hostApi = PlatformImagesApi();

/// Sets the [PlatformImagesApi] instance used to implement the static methods
/// of [IosPlatformImages].
///
/// This exists only for unit tests.
@visibleForTesting
void setPlatformImageHostApi(PlatformImagesApi api) {
_hostApi = api;
}

// ignore: avoid_classes_with_only_static_members
/// Class to help loading of iOS platform images into Flutter.
///
/// For example, loading an image that is in `Assets.xcassts`.
class IosPlatformImages {
static const MethodChannel _channel =
MethodChannel('plugins.flutter.io/ios_platform_images');

/// Loads an image from asset catalogs. The equivalent would be:
/// `[UIImage imageNamed:name]`.
///
/// Throws an exception if the image can't be found.
///
/// See [https://developer.apple.com/documentation/uikit/uiimage/1624146-imagenamed?language=objc]
static ImageProvider load(String name) {
final Future<Map<String, dynamic>?> loadInfo =
_channel.invokeMapMethod<String, dynamic>('loadImage', name);
final Future<PlatformImageData?> imageData = _hostApi.loadImage(name);
final Completer<Uint8List> bytesCompleter = Completer<Uint8List>();
final Completer<double> scaleCompleter = Completer<double>();
loadInfo.then((Map<String, dynamic>? map) {
if (map == null) {
imageData.then((PlatformImageData? image) {
if (image == null) {
scaleCompleter.completeError(
Exception("Image couldn't be found: $name"),
);
Expand All @@ -130,8 +144,8 @@ class IosPlatformImages {
);
return;
}
scaleCompleter.complete(map['scale']! as double);
bytesCompleter.complete(map['data']! as Uint8List);
scaleCompleter.complete(image.scale);
bytesCompleter.complete(image.data);
});
return _FutureMemoryImage(bytesCompleter.future, scaleCompleter.future);
}
Expand All @@ -143,7 +157,6 @@ class IosPlatformImages {
///
/// See [https://developer.apple.com/documentation/foundation/nsbundle/1411540-urlforresource?language=objc]
static Future<String?> resolveURL(String name, {String? extension}) {
return _channel
.invokeMethod<String>('resolveURL', <Object?>[name, extension]);
return _hostApi.resolveUrl(name, extension);
}
}
Loading

0 comments on commit 51e74b9

Please sign in to comment.