Skip to content

Commit 22b559c

Browse files
bc-leeTecHaxter
authored andcommitted
[pigeon][swift] Removes FlutterError in favor of PigeonError (flutter#6611)
Swift has more strict type system than Objective-C. For example, protocol conformance must not be redundant[1]. Also, Both FlutterError and Swift.Error are public, extension `FlutterError` to `Swift.Error` is also public. If someone create a such extension in the plugin code, Flutter app can't create a such extension in the app code, which forces Plugin developers to use Objective-C when they want to use pigeon, instead of Swift. To avoid this issue, this change makes pigeon to use another error type, named `PigeonError`, instead of `FlutterError`. By declaring `PigeonError` as internal visibility, their existence won't make compilation error even if `PigeonError` is declared in the app code. Fixes flutter/flutter#147371. See also flutter/flutter#137057 (comment). [1] https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/#Protocol-Conformance-Must-Not-Be-Redundant
1 parent c7c5643 commit 22b559c

File tree

16 files changed

+396
-278
lines changed

16 files changed

+396
-278
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 19.0.0
22

3+
* **Breaking Change** [swift] Removes `FlutterError` in favor of `PigeonError`.
34
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.
45

56
## 18.0.1

packages/pigeon/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ should be returned via the provided callback.
5353
To pass custom details into `PlatformException` for error handling,
5454
use `FlutterError` in your Host API. [Example](./example/README.md#HostApi_Example).
5555

56-
To use `FlutterError` in Swift you must first extend a standard error.
57-
[Example](./example/README.md#AppDelegate.swift).
56+
For swift, use `PigeonError` instead of `FlutterError` when throwing an error. See [Example#Swift](./example/README.md#Swift) for more details.
5857

5958
#### Objective-C and C++
6059

packages/pigeon/example/README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,24 @@ Future<bool> sendMessage(String messageText) {
115115
### Swift
116116

117117
This is the code that will use the generated Swift code to receive calls from Flutter.
118-
packages/pigeon/example/app/ios/Runner/AppDelegate.swift
118+
Unlike other languages, when throwing an error, use `PigeonError` instead of `FlutterError`, as `FlutterError` does not conform to `Swift.Error`.
119119
<?code-excerpt "ios/Runner/AppDelegate.swift (swift-class)"?>
120120
```swift
121-
// This extension of Error is required to do use FlutterError in any Swift code.
122-
extension FlutterError: Error {}
123-
124121
private class PigeonApiImplementation: ExampleHostApi {
125122
func getHostLanguage() throws -> String {
126123
return "Swift"
127124
}
128125

129126
func add(_ a: Int64, to b: Int64) throws -> Int64 {
130127
if a < 0 || b < 0 {
131-
throw FlutterError(code: "code", message: "message", details: "details")
128+
throw PigeonError(code: "code", message: "message", details: "details")
132129
}
133130
return a + b
134131
}
135132

136133
func sendMessage(message: MessageData, completion: @escaping (Result<Bool, Error>) -> Void) {
137134
if message.code == Code.one {
138-
completion(.failure(FlutterError(code: "code", message: "message", details: "details")))
135+
completion(.failure(PigeonError(code: "code", message: "message", details: "details")))
139136
return
140137
}
141138
completion(.success(true))

packages/pigeon/example/app/ios/Runner/AppDelegate.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,21 @@ import Flutter
66
import UIKit
77

88
// #docregion swift-class
9-
// This extension of Error is required to do use FlutterError in any Swift code.
10-
extension FlutterError: Error {}
11-
129
private class PigeonApiImplementation: ExampleHostApi {
1310
func getHostLanguage() throws -> String {
1411
return "Swift"
1512
}
1613

1714
func add(_ a: Int64, to b: Int64) throws -> Int64 {
1815
if a < 0 || b < 0 {
19-
throw FlutterError(code: "code", message: "message", details: "details")
16+
throw PigeonError(code: "code", message: "message", details: "details")
2017
}
2118
return a + b
2219
}
2320

2421
func sendMessage(message: MessageData, completion: @escaping (Result<Bool, Error>) -> Void) {
2522
if message.code == Code.one {
26-
completion(.failure(FlutterError(code: "code", message: "message", details: "details")))
23+
completion(.failure(PigeonError(code: "code", message: "message", details: "details")))
2724
return
2825
}
2926
completion(.success(true))

packages/pigeon/example/app/ios/Runner/Messages.g.swift

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,36 @@ import Foundation
1414
#error("Unsupported platform.")
1515
#endif
1616

17+
/// Error class for passing custom error details to Dart side.
18+
final class PigeonError: Error {
19+
let code: String
20+
let message: String?
21+
let details: Any?
22+
23+
init(code: String, message: String?, details: Any?) {
24+
self.code = code
25+
self.message = message
26+
self.details = details
27+
}
28+
29+
var localizedDescription: String {
30+
return
31+
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
32+
}
33+
}
34+
1735
private func wrapResult(_ result: Any?) -> [Any?] {
1836
return [result]
1937
}
2038

2139
private func wrapError(_ error: Any) -> [Any?] {
40+
if let pigeonError = error as? PigeonError {
41+
return [
42+
pigeonError.code,
43+
pigeonError.message,
44+
pigeonError.details,
45+
]
46+
}
2247
if let flutterError = error as? FlutterError {
2348
return [
2449
flutterError.code,
@@ -33,8 +58,8 @@ private func wrapError(_ error: Any) -> [Any?] {
3358
]
3459
}
3560

36-
private func createConnectionError(withChannelName channelName: String) -> FlutterError {
37-
return FlutterError(
61+
private func createConnectionError(withChannelName channelName: String) -> PigeonError {
62+
return PigeonError(
3863
code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.",
3964
details: "")
4065
}
@@ -194,7 +219,7 @@ class ExampleHostApiSetup {
194219
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
195220
protocol MessageFlutterApiProtocol {
196221
func flutterMethod(
197-
aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void)
222+
aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void)
198223
}
199224
class MessageFlutterApi: MessageFlutterApiProtocol {
200225
private let binaryMessenger: FlutterBinaryMessenger
@@ -204,7 +229,7 @@ class MessageFlutterApi: MessageFlutterApiProtocol {
204229
self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
205230
}
206231
func flutterMethod(
207-
aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void
232+
aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void
208233
) {
209234
let channelName: String =
210235
"dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod\(messageChannelSuffix)"
@@ -218,11 +243,11 @@ class MessageFlutterApi: MessageFlutterApiProtocol {
218243
let code: String = listResponse[0] as! String
219244
let message: String? = nilOrValue(listResponse[1])
220245
let details: String? = nilOrValue(listResponse[2])
221-
completion(.failure(FlutterError(code: code, message: message, details: details)))
246+
completion(.failure(PigeonError(code: code, message: message, details: details)))
222247
} else if listResponse[0] == nil {
223248
completion(
224249
.failure(
225-
FlutterError(
250+
PigeonError(
226251
code: "null-error",
227252
message: "Flutter api returned null value for non-null return value.", details: "")))
228253
} else {

packages/pigeon/lib/generator_tools.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import 'ast.dart';
1313
/// The current version of pigeon.
1414
///
1515
/// This must match the version in pubspec.yaml.
16-
const String pigeonVersion = '18.0.1';
16+
const String pigeonVersion = '19.0.0';
1717

1818
/// Prefix for all local variables in methods.
1919
///

packages/pigeon/lib/pigeon_lib.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ class SwiftGeneratorAdapter implements GeneratorAdapter {
705705
? _lineReader(
706706
path.posix.join(options.basePath ?? '', options.copyrightHeader))
707707
: null,
708+
errorClassName: swiftOptions.errorClassName,
708709
));
709710
const SwiftGenerator generator = SwiftGenerator();
710711
generator.generate(

packages/pigeon/lib/swift_generator.dart

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ class SwiftOptions {
1919
/// Creates a [SwiftOptions] object
2020
const SwiftOptions({
2121
this.copyrightHeader,
22+
this.errorClassName,
2223
});
2324

2425
/// A copyright header that will get prepended to generated code.
2526
final Iterable<String>? copyrightHeader;
2627

28+
/// The name of the error class used for passing custom error parameters.
29+
final String? errorClassName;
30+
2731
/// Creates a [SwiftOptions] from a Map representation where:
2832
/// `x = SwiftOptions.fromList(x.toMap())`.
2933
static SwiftOptions fromList(Map<String, Object> map) {
3034
return SwiftOptions(
3135
copyrightHeader: map['copyrightHeader'] as Iterable<String>?,
36+
errorClassName: map['errorClassName'] as String?,
3237
);
3338
}
3439

@@ -37,6 +42,7 @@ class SwiftOptions {
3742
Map<String, Object> toMap() {
3843
final Map<String, Object> result = <String, Object>{
3944
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
45+
if (errorClassName != null) 'errorClassName': errorClassName!,
4046
};
4147
return result;
4248
}
@@ -316,7 +322,7 @@ class SwiftGenerator extends StructuredGenerator<SwiftOptions> {
316322
name: func.name,
317323
parameters: func.parameters,
318324
returnType: func.returnType,
319-
errorTypeName: 'FlutterError',
325+
errorTypeName: _getErrorClassName(generatorOptions),
320326
isAsynchronous: true,
321327
swiftFunction: func.swiftFunction,
322328
getParameterName: _getSafeArgumentName,
@@ -350,6 +356,7 @@ class SwiftGenerator extends StructuredGenerator<SwiftOptions> {
350356
indent, func.documentationComments, _docCommentSpec);
351357
_writeFlutterMethod(
352358
indent,
359+
generatorOptions: generatorOptions,
353360
name: func.name,
354361
channelName: makeChannelName(api, func, dartPackageName),
355362
parameters: func.parameters,
@@ -614,10 +621,20 @@ class SwiftGenerator extends StructuredGenerator<SwiftOptions> {
614621
});
615622
}
616623

617-
void _writeWrapError(Indent indent) {
624+
void _writeWrapError(SwiftOptions generatorOptions, Indent indent) {
618625
indent.newln();
619626
indent.write('private func wrapError(_ error: Any) -> [Any?] ');
620627
indent.addScoped('{', '}', () {
628+
indent.write(
629+
'if let pigeonError = error as? ${_getErrorClassName(generatorOptions)} ');
630+
indent.addScoped('{', '}', () {
631+
indent.write('return ');
632+
indent.addScoped('[', ']', () {
633+
indent.writeln('pigeonError.code,');
634+
indent.writeln('pigeonError.message,');
635+
indent.writeln('pigeonError.details,');
636+
});
637+
});
621638
indent.write('if let flutterError = error as? FlutterError ');
622639
indent.addScoped('{', '}', () {
623640
indent.write('return ');
@@ -645,13 +662,14 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
645662
}''');
646663
}
647664

648-
void _writeCreateConnectionError(Indent indent) {
665+
void _writeCreateConnectionError(
666+
SwiftOptions generatorOptions, Indent indent) {
649667
indent.newln();
650668
indent.writeScoped(
651-
'private func createConnectionError(withChannelName channelName: String) -> FlutterError {',
669+
'private func createConnectionError(withChannelName channelName: String) -> ${_getErrorClassName(generatorOptions)} {',
652670
'}', () {
653671
indent.writeln(
654-
'return FlutterError(code: "channel-error", message: "Unable to establish connection on channel: \'\\(channelName)\'.", details: "")');
672+
'return ${_getErrorClassName(generatorOptions)}(code: "channel-error", message: "Unable to establish connection on channel: \'\\(channelName)\'.", details: "")');
655673
});
656674
}
657675

@@ -669,19 +687,23 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
669687
.whereType<AstFlutterApi>()
670688
.any((Api api) => api.methods.isNotEmpty);
671689

690+
_writePigeonError(generatorOptions, indent);
691+
672692
if (hasHostApi) {
673693
_writeWrapResult(indent);
674-
_writeWrapError(indent);
694+
_writeWrapError(generatorOptions, indent);
675695
}
676696
if (hasFlutterApi) {
677-
_writeCreateConnectionError(indent);
697+
_writeCreateConnectionError(generatorOptions, indent);
678698
}
699+
679700
_writeIsNullish(indent);
680701
_writeNilOrValue(indent);
681702
}
682703

683704
void _writeFlutterMethod(
684705
Indent indent, {
706+
required SwiftOptions generatorOptions,
685707
required String name,
686708
required String channelName,
687709
required List<Parameter> parameters,
@@ -693,7 +715,7 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
693715
name: name,
694716
parameters: parameters,
695717
returnType: returnType,
696-
errorTypeName: 'FlutterError',
718+
errorTypeName: _getErrorClassName(generatorOptions),
697719
isAsynchronous: true,
698720
swiftFunction: swiftFunction,
699721
getParameterName: _getSafeArgumentName,
@@ -735,12 +757,12 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
735757
indent.writeln('let message: String? = nilOrValue(listResponse[1])');
736758
indent.writeln('let details: String? = nilOrValue(listResponse[2])');
737759
indent.writeln(
738-
'completion(.failure(FlutterError(code: code, message: message, details: details)))');
760+
'completion(.failure(${_getErrorClassName(generatorOptions)}(code: code, message: message, details: details)))');
739761
}, addTrailingNewline: false);
740762
if (!returnType.isNullable && !returnType.isVoid) {
741763
indent.addScoped('else if listResponse[0] == nil {', '} ', () {
742764
indent.writeln(
743-
'completion(.failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))');
765+
'completion(.failure(${_getErrorClassName(generatorOptions)}(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))');
744766
}, addTrailingNewline: false);
745767
}
746768
indent.addScoped('else {', '}', () {
@@ -870,11 +892,41 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
870892
indent.writeln('$varChannelName.setMessageHandler(nil)');
871893
});
872894
}
895+
896+
void _writePigeonError(SwiftOptions generatorOptions, Indent indent) {
897+
indent.newln();
898+
indent.writeln(
899+
'/// Error class for passing custom error details to Dart side.');
900+
indent.writeScoped(
901+
'final class ${_getErrorClassName(generatorOptions)}: Error {', '}',
902+
() {
903+
indent.writeln('let code: String');
904+
indent.writeln('let message: String?');
905+
indent.writeln('let details: Any?');
906+
indent.newln();
907+
indent.writeScoped(
908+
'init(code: String, message: String?, details: Any?) {', '}', () {
909+
indent.writeln('self.code = code');
910+
indent.writeln('self.message = message');
911+
indent.writeln('self.details = details');
912+
});
913+
indent.newln();
914+
indent.writeScoped('var localizedDescription: String {', '}', () {
915+
indent.writeScoped('return', '', () {
916+
indent.writeln(
917+
'"${_getErrorClassName(generatorOptions)}(code: \\(code), message: \\(message ?? "<nil>"), details: \\(details ?? "<nil>")"');
918+
}, addTrailingNewline: false);
919+
});
920+
});
921+
}
873922
}
874923

875924
/// Calculates the name of the codec that will be generated for [api].
876925
String _getCodecName(Api api) => '${api.name}Codec';
877926

927+
String _getErrorClassName(SwiftOptions generatorOptions) =>
928+
generatorOptions.errorClassName ?? 'PigeonError';
929+
878930
String _getArgumentName(int count, NamedType argument) =>
879931
argument.name.isEmpty ? 'arg$count' : argument.name;
880932

packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,16 @@ class RunnerTests: XCTestCase {
4848
}
4949

5050
class FlutterApiFromProtocol: FlutterSmallApiProtocol {
51-
func echo(string aStringArg: String, completion: @escaping (Result<String, FlutterError>) -> Void)
52-
{
51+
func echo(
52+
string aStringArg: String,
53+
completion: @escaping (Result<String, test_plugin.PigeonError>) -> Void
54+
) {
5355
completion(.success(aStringArg))
5456
}
5557

5658
func echo(
5759
_ msgArg: test_plugin.TestMessage,
58-
completion: @escaping (Result<test_plugin.TestMessage, FlutterError>) -> Void
60+
completion: @escaping (Result<test_plugin.TestMessage, test_plugin.PigeonError>) -> Void
5961
) {
6062
completion(.success(msgArg))
6163
}

0 commit comments

Comments
 (0)