SwiftUIとFlutter 間のタイプセーフなデータ通信
SwiftUIとFlutter間でpigeon
を用いたタイプセーフなデータ通信を一から構築したいと思います。
Flutter → SwiftUI
SwiftUI → Flutter のどちらも対応します。
こんな人におすすめ
・pigeonを用いてFlutterでNative連携したい
・@HostApi()と@FlutterApi()の使い方がいまいちわからない
・FlutterでSwiftUIを呼び出しているのをみてみたい
・インターフェイスやプロトコルから、実装を理解したい
⭐️ 以下のように、 Pigeonを用いてデータ通信が可能になります。
全体の流れ
① 初期化処理
main.dartでFlutter APIをセットアップし、具象クラスを適用する。
② FlutterからSwift API呼び出し:
Flutter側でSwift APIを呼び出し、モーダル遷移でSwiftUIのViewが表示され、メッセージを待つ。
③ SwiftからFlutterAPI呼び出し
モーダル遷移が立ち上がると、Swift側でFlutter APIを呼び出す。
④ 結果を返す
モーダル遷移が閉じられると、SwiftUIのテキストに格納された文字がFlutter側の結果に返される。
リポジトリはこちらです。
Pigeon とは
→ Flutter とネイティブプラットフォーム間の通信を型安全かつ容易かつ高速にするためのコード生成ツールです。
wifiやBurtoothなど、Flutterでもネイティブの機能にアクセスしたいときが、あると思います。
そして、それは以前、MethodChannelを通じて行われていました。
しかし、MethodChannelは型安全ではありません。
コーディング手順
1. packageを作成する
以下のコマンドで、プロジェクトを作成する。
nativesampleは適宜変えてください
$ flutter create --org co.com.nativesample --template=plugin --platforms=android,ios nativesample --project-name nativesample
2. ルートに pigeons/messages.dartを作成
$ mkdir pigeons && touch pigeons/messages.dart
3. messages.dart に以下のコードを記述
このコードは、Pigeonというツールを使って、Dartと他のネイティブ言語との間で相互に通信するためのコードを自動生成するための設定を行っています。
💡下記の場合、@HostApi()はSwiftのプロトコルを、@FlutterApi()では、Dartのインターフェイスを定義しているのですね!
import 'package:pigeon/pigeon.dart';
(PigeonOptions(
dartOut: 'lib/src/generated/messages.g.dart',
dartOptions: DartOptions(),
swiftOut: 'ios/Classes/Messages.swift',
swiftOptions: SwiftOptions(),
))
class Message {
String? message;
}
/// FlutterがNativeのAPIを呼び出す
/// Swift側で具象クラスを実装する必要がある
()
abstract class SwiftApiClass {
Message hostApi();
}
/// NativeがFlutterのAPIを呼び出す
/// Flutter側で具象クラスを実装する必要がある
()
abstract class c {
Message flutterApi();
}
4. pigeon 生成コマンド
以下のコマンドで、上記で指定したファイルを自動生成します。
$ flutter pub run pigeon --input pigeons/messages.dart
5. Messages.swiftの具象クラスを定義する
Messages.swift
が生成されたかと思います。
そこには@HostApiに基づいたSwiftのプロトコルが定義されています。
ですので、次は具象クラスを定義することです。
このタイミングで、SwiftUIのViewとデータをどのようにやり取りするかを決めます。SwiftUIのViewとデータをやり取りするには、メソッドの中でSwiftUIのViewを表示し、ユーザーからの入力を取得してFlutterに返す処理を実装します。
詳しくは、こちらをご覧ください。
以下の内容を実装してます。
- Flutter APIの呼び出し
- SwiftUIのViewと 具象クラスのデータやり取り
- モーダル遷移のopen/close
@available(iOS 13.0, *)
class MessagesImpl: NSObject, SwiftApiClass {
func hostApi(completion: @escaping (Result<Message, Error>) -> Void) {
/// ここに hostApiの実装をかく
6. NativesamplePlugin.swiftを修正
💡 MessagesImplクラスをSwiftのAPIとして設定します
public class NativesamplePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "nativesample", binaryMessenger: registrar.messenger())
let instance = NativesamplePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
let messenger = registrar.messenger()
// MessagesImplクラスをSwiftのAPIとして設定
if #available(iOS 13.0, *) {
SwiftApiClassSetup.setUp(binaryMessenger: messenger, api: MessagesImpl())
} else {
print("iOS 13.0未満のデバイスではMessagesImplはサポートされていません。")
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}
こちらで、Swift側の設定は終わりです。
7. Flutter側の実装
最後に main.dart
で以下を行います。
詳しくは、こちらをご参照ください。
- インターフェイスの実装クラスの定義
- Flutter APIの セットアップ
- Swift APIの呼び出し
message.g.dart
を見ると、
message.dart
で定義した @FlutterApi() の関数は、
abstract になったままなことが確認できます。
と言うことは、Dart側でインターフェイスを実装したクラスを定義する必要があると言うことです。
// @HostApi()
class SwiftApiClass {
// @FlutterApi()
abstract class FlutterApiClass {
インターフェイスの実装クラス定義と、FlutterAPIのセットアップ
class _HomeScreenState extends State<HomeScreen> {
String? _message;
/// [FlutterApiClass] をセットアップ
/// [FlutterApiClassImpl] で具象クラスを実装
void initState() {
super.initState();
FlutterApiClass.setUp(FlutterApiClassImpl());
...
/// Flutter側で実装されたAPIをNative側で呼び出す
/// Flutter側で具象クラスを実装する必要がある
/// [MessagesImpl.swift] で呼び出された際に
/// [flutterApi] が呼び出されるように今回はコーディングした
class FlutterApiClassImpl implements FlutterApiClass {
Future<Message> flutterApi() {
final message = Message();
message.message = "こんにちは! Flutterからのメッセージです。";
return Future.value(message);
}
}
Swift APIの呼び出しはこんな感じ
Future<void> _fetchMessage() async {
final api = SwiftApiClass();
try {
final message = await api.hostApi();
setState(() {
_message = message.message;
});
} catch (e) {
print("Error: $e");
}
}
まとめ
SwiftUIとFlutterの連携が完了しました。
かなり、手間がかかりますよね。
しかし、セキュリティ要件を満たすときや、高度なカメラ機能など、Nativeに頼る機会もあると思います。
また、いろんな書き方ができたり、Objective-CやJavaなどでも実装できたりと、広いので頑張りたいですね!
参考になれば幸いです!
Discussion