Skip to content

Commit 51e74b9

Browse files
[ios_platform_images] Convert to Pigeon (#4945)
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
1 parent d08ebfd commit 51e74b9

File tree

13 files changed

+482
-65
lines changed

13 files changed

+482
-65
lines changed

packages/ios_platform_images/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.2+3
2+
3+
* Converts platform communication to Pigeon.
4+
15
## 0.2.2+2
26

37
* Adds pub topics to package metadata.

packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@
222222
97C146E61CF9000F007C117D /* Project object */ = {
223223
isa = PBXProject;
224224
attributes = {
225-
LastUpgradeCheck = 1300;
225+
LastUpgradeCheck = 1430;
226226
ORGANIZATIONNAME = "The Flutter Authors";
227227
TargetAttributes = {
228228
97C146ED1CF9000F007C117D = {

packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1300"
3+
LastUpgradeVersion = "1430"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
#import <Flutter/Flutter.h>
66

7+
#import "messages.g.h"
8+
79
/// A plugin for Flutter that allows Flutter to load images in a platform
810
/// specific way on iOS.
9-
@interface IosPlatformImagesPlugin : NSObject <FlutterPlugin>
11+
@interface IosPlatformImagesPlugin : NSObject <FlutterPlugin, FPIPlatformImagesApi>
1012
@end

packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,27 @@ @interface IosPlatformImagesPlugin ()
1414
@implementation IosPlatformImagesPlugin
1515

1616
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
17-
FlutterMethodChannel *channel =
18-
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/ios_platform_images"
19-
binaryMessenger:[registrar messenger]];
20-
21-
[channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
22-
if ([@"loadImage" isEqualToString:call.method]) {
23-
NSString *name = call.arguments;
24-
UIImage *image = [UIImage imageNamed:name];
25-
NSData *data = UIImagePNGRepresentation(image);
26-
if (data) {
27-
result(@{
28-
@"scale" : @(image.scale),
29-
@"data" : [FlutterStandardTypedData typedDataWithBytes:data],
30-
});
31-
} else {
32-
result(nil);
33-
}
34-
return;
35-
} else if ([@"resolveURL" isEqualToString:call.method]) {
36-
NSArray *args = call.arguments;
37-
NSString *name = args[0];
38-
NSString *extension = (args[1] == (id)NSNull.null) ? nil : args[1];
39-
40-
NSURL *url = [[NSBundle mainBundle] URLForResource:name withExtension:extension];
41-
result(url.absoluteString);
42-
return;
43-
}
44-
result(FlutterMethodNotImplemented);
45-
}];
17+
FPIPlatformImagesApiSetup(registrar.messenger, [[IosPlatformImagesPlugin alloc] init]);
18+
}
19+
20+
- (nullable FPIPlatformImageData *)
21+
loadImageWithName:(nonnull NSString *)name
22+
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
23+
UIImage *image = [UIImage imageNamed:name];
24+
NSData *data = UIImagePNGRepresentation(image);
25+
if (!data) {
26+
return nil;
27+
}
28+
return [FPIPlatformImageData makeWithData:[FlutterStandardTypedData typedDataWithBytes:data]
29+
scale:@(image.scale)];
30+
}
31+
32+
- (nullable NSString *)resolveURLForResource:(nonnull NSString *)name
33+
withExtension:(nullable NSString *)extension
34+
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)
35+
error {
36+
NSURL *url = [[NSBundle mainBundle] URLForResource:name withExtension:extension];
37+
return url.absoluteString;
4638
}
4739

4840
@end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
5+
// See also: https://pub.dev/packages/pigeon
6+
7+
#import <Foundation/Foundation.h>
8+
9+
@protocol FlutterBinaryMessenger;
10+
@protocol FlutterMessageCodec;
11+
@class FlutterError;
12+
@class FlutterStandardTypedData;
13+
14+
NS_ASSUME_NONNULL_BEGIN
15+
16+
@class FPIPlatformImageData;
17+
18+
/// A serialization of a platform image's data.
19+
@interface FPIPlatformImageData : NSObject
20+
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
21+
- (instancetype)init NS_UNAVAILABLE;
22+
+ (instancetype)makeWithData:(FlutterStandardTypedData *)data scale:(NSNumber *)scale;
23+
/// The image data.
24+
@property(nonatomic, strong) FlutterStandardTypedData *data;
25+
/// The image's scale factor.
26+
@property(nonatomic, strong) NSNumber *scale;
27+
@end
28+
29+
/// The codec used by FPIPlatformImagesApi.
30+
NSObject<FlutterMessageCodec> *FPIPlatformImagesApiGetCodec(void);
31+
32+
@protocol FPIPlatformImagesApi
33+
/// Returns the URL for the given resource, or null if no such resource is
34+
/// found.
35+
- (nullable NSString *)resolveURLForResource:(NSString *)resourceName
36+
withExtension:(nullable NSString *)extension
37+
error:(FlutterError *_Nullable *_Nonnull)error;
38+
/// Returns the data for the image resource with the given name, or null if
39+
/// no such resource is found.
40+
- (nullable FPIPlatformImageData *)loadImageWithName:(NSString *)name
41+
error:(FlutterError *_Nullable *_Nonnull)error;
42+
@end
43+
44+
extern void FPIPlatformImagesApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
45+
NSObject<FPIPlatformImagesApi> *_Nullable api);
46+
47+
NS_ASSUME_NONNULL_END
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
5+
// See also: https://pub.dev/packages/pigeon
6+
7+
#import "messages.g.h"
8+
9+
#if TARGET_OS_OSX
10+
#import <FlutterMacOS/FlutterMacOS.h>
11+
#else
12+
#import <Flutter/Flutter.h>
13+
#endif
14+
15+
#if !__has_feature(objc_arc)
16+
#error File requires ARC to be enabled.
17+
#endif
18+
19+
static NSArray *wrapResult(id result, FlutterError *error) {
20+
if (error) {
21+
return @[
22+
error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null]
23+
];
24+
}
25+
return @[ result ?: [NSNull null] ];
26+
}
27+
static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
28+
id result = array[key];
29+
return (result == [NSNull null]) ? nil : result;
30+
}
31+
32+
@interface FPIPlatformImageData ()
33+
+ (FPIPlatformImageData *)fromList:(NSArray *)list;
34+
+ (nullable FPIPlatformImageData *)nullableFromList:(NSArray *)list;
35+
- (NSArray *)toList;
36+
@end
37+
38+
@implementation FPIPlatformImageData
39+
+ (instancetype)makeWithData:(FlutterStandardTypedData *)data scale:(NSNumber *)scale {
40+
FPIPlatformImageData *pigeonResult = [[FPIPlatformImageData alloc] init];
41+
pigeonResult.data = data;
42+
pigeonResult.scale = scale;
43+
return pigeonResult;
44+
}
45+
+ (FPIPlatformImageData *)fromList:(NSArray *)list {
46+
FPIPlatformImageData *pigeonResult = [[FPIPlatformImageData alloc] init];
47+
pigeonResult.data = GetNullableObjectAtIndex(list, 0);
48+
NSAssert(pigeonResult.data != nil, @"");
49+
pigeonResult.scale = GetNullableObjectAtIndex(list, 1);
50+
NSAssert(pigeonResult.scale != nil, @"");
51+
return pigeonResult;
52+
}
53+
+ (nullable FPIPlatformImageData *)nullableFromList:(NSArray *)list {
54+
return (list) ? [FPIPlatformImageData fromList:list] : nil;
55+
}
56+
- (NSArray *)toList {
57+
return @[
58+
(self.data ?: [NSNull null]),
59+
(self.scale ?: [NSNull null]),
60+
];
61+
}
62+
@end
63+
64+
@interface FPIPlatformImagesApiCodecReader : FlutterStandardReader
65+
@end
66+
@implementation FPIPlatformImagesApiCodecReader
67+
- (nullable id)readValueOfType:(UInt8)type {
68+
switch (type) {
69+
case 128:
70+
return [FPIPlatformImageData fromList:[self readValue]];
71+
default:
72+
return [super readValueOfType:type];
73+
}
74+
}
75+
@end
76+
77+
@interface FPIPlatformImagesApiCodecWriter : FlutterStandardWriter
78+
@end
79+
@implementation FPIPlatformImagesApiCodecWriter
80+
- (void)writeValue:(id)value {
81+
if ([value isKindOfClass:[FPIPlatformImageData class]]) {
82+
[self writeByte:128];
83+
[self writeValue:[value toList]];
84+
} else {
85+
[super writeValue:value];
86+
}
87+
}
88+
@end
89+
90+
@interface FPIPlatformImagesApiCodecReaderWriter : FlutterStandardReaderWriter
91+
@end
92+
@implementation FPIPlatformImagesApiCodecReaderWriter
93+
- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
94+
return [[FPIPlatformImagesApiCodecWriter alloc] initWithData:data];
95+
}
96+
- (FlutterStandardReader *)readerWithData:(NSData *)data {
97+
return [[FPIPlatformImagesApiCodecReader alloc] initWithData:data];
98+
}
99+
@end
100+
101+
NSObject<FlutterMessageCodec> *FPIPlatformImagesApiGetCodec(void) {
102+
static FlutterStandardMessageCodec *sSharedObject = nil;
103+
static dispatch_once_t sPred = 0;
104+
dispatch_once(&sPred, ^{
105+
FPIPlatformImagesApiCodecReaderWriter *readerWriter =
106+
[[FPIPlatformImagesApiCodecReaderWriter alloc] init];
107+
sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
108+
});
109+
return sSharedObject;
110+
}
111+
112+
void FPIPlatformImagesApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
113+
NSObject<FPIPlatformImagesApi> *api) {
114+
/// Returns the URL for the given resource, or null if no such resource is
115+
/// found.
116+
{
117+
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
118+
initWithName:@"dev.flutter.pigeon.ios_platform_images.PlatformImagesApi.resolveUrl"
119+
binaryMessenger:binaryMessenger
120+
codec:FPIPlatformImagesApiGetCodec()];
121+
if (api) {
122+
NSCAssert([api respondsToSelector:@selector(resolveURLForResource:withExtension:error:)],
123+
@"FPIPlatformImagesApi api (%@) doesn't respond to "
124+
@"@selector(resolveURLForResource:withExtension:error:)",
125+
api);
126+
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
127+
NSArray *args = message;
128+
NSString *arg_resourceName = GetNullableObjectAtIndex(args, 0);
129+
NSString *arg_extension = GetNullableObjectAtIndex(args, 1);
130+
FlutterError *error;
131+
NSString *output = [api resolveURLForResource:arg_resourceName
132+
withExtension:arg_extension
133+
error:&error];
134+
callback(wrapResult(output, error));
135+
}];
136+
} else {
137+
[channel setMessageHandler:nil];
138+
}
139+
}
140+
/// Returns the data for the image resource with the given name, or null if
141+
/// no such resource is found.
142+
{
143+
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
144+
initWithName:@"dev.flutter.pigeon.ios_platform_images.PlatformImagesApi.loadImage"
145+
binaryMessenger:binaryMessenger
146+
codec:FPIPlatformImagesApiGetCodec()];
147+
if (api) {
148+
NSCAssert(
149+
[api respondsToSelector:@selector(loadImageWithName:error:)],
150+
@"FPIPlatformImagesApi api (%@) doesn't respond to @selector(loadImageWithName:error:)",
151+
api);
152+
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
153+
NSArray *args = message;
154+
NSString *arg_name = GetNullableObjectAtIndex(args, 0);
155+
FlutterError *error;
156+
FPIPlatformImageData *output = [api loadImageWithName:arg_name error:&error];
157+
callback(wrapResult(output, error));
158+
}];
159+
} else {
160+
[channel setMessageHandler:nil];
161+
}
162+
}
163+
}

packages/ios_platform_images/lib/ios_platform_images.dart

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import 'dart:async';
66
import 'dart:ui' as ui;
77

88
import 'package:flutter/foundation.dart'
9-
show SynchronousFuture, describeIdentity, immutable, objectRuntimeType;
9+
show
10+
SynchronousFuture,
11+
describeIdentity,
12+
immutable,
13+
objectRuntimeType,
14+
visibleForTesting;
1015
import 'package:flutter/rendering.dart';
1116
import 'package:flutter/services.dart';
1217

18+
import 'src/messages.g.dart';
19+
1320
class _FutureImageStreamCompleter extends ImageStreamCompleter {
1421
_FutureImageStreamCompleter({
1522
required Future<ui.Codec> codec,
@@ -101,27 +108,34 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> {
101108
'(${describeIdentity(_futureBytes)}, scale: $_futureScale)';
102109
}
103110

111+
PlatformImagesApi _hostApi = PlatformImagesApi();
112+
113+
/// Sets the [PlatformImagesApi] instance used to implement the static methods
114+
/// of [IosPlatformImages].
115+
///
116+
/// This exists only for unit tests.
117+
@visibleForTesting
118+
void setPlatformImageHostApi(PlatformImagesApi api) {
119+
_hostApi = api;
120+
}
121+
104122
// ignore: avoid_classes_with_only_static_members
105123
/// Class to help loading of iOS platform images into Flutter.
106124
///
107125
/// For example, loading an image that is in `Assets.xcassts`.
108126
class IosPlatformImages {
109-
static const MethodChannel _channel =
110-
MethodChannel('plugins.flutter.io/ios_platform_images');
111-
112127
/// Loads an image from asset catalogs. The equivalent would be:
113128
/// `[UIImage imageNamed:name]`.
114129
///
115130
/// Throws an exception if the image can't be found.
116131
///
117132
/// See [https://developer.apple.com/documentation/uikit/uiimage/1624146-imagenamed?language=objc]
118133
static ImageProvider load(String name) {
119-
final Future<Map<String, dynamic>?> loadInfo =
120-
_channel.invokeMapMethod<String, dynamic>('loadImage', name);
134+
final Future<PlatformImageData?> imageData = _hostApi.loadImage(name);
121135
final Completer<Uint8List> bytesCompleter = Completer<Uint8List>();
122136
final Completer<double> scaleCompleter = Completer<double>();
123-
loadInfo.then((Map<String, dynamic>? map) {
124-
if (map == null) {
137+
imageData.then((PlatformImageData? image) {
138+
if (image == null) {
125139
scaleCompleter.completeError(
126140
Exception("Image couldn't be found: $name"),
127141
);
@@ -130,8 +144,8 @@ class IosPlatformImages {
130144
);
131145
return;
132146
}
133-
scaleCompleter.complete(map['scale']! as double);
134-
bytesCompleter.complete(map['data']! as Uint8List);
147+
scaleCompleter.complete(image.scale);
148+
bytesCompleter.complete(image.data);
135149
});
136150
return _FutureMemoryImage(bytesCompleter.future, scaleCompleter.future);
137151
}
@@ -143,7 +157,6 @@ class IosPlatformImages {
143157
///
144158
/// See [https://developer.apple.com/documentation/foundation/nsbundle/1411540-urlforresource?language=objc]
145159
static Future<String?> resolveURL(String name, {String? extension}) {
146-
return _channel
147-
.invokeMethod<String>('resolveURL', <Object?>[name, extension]);
160+
return _hostApi.resolveUrl(name, extension);
148161
}
149162
}

0 commit comments

Comments
 (0)