🔥

SwiftUIとFlutter 間のタイプセーフなデータ通信

2024/08/13に公開

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側の結果に返される。

リポジトリはこちらです。
https://github.com/rensawamo/flutter_swiftUI

Pigeon とは

→ Flutter とネイティブプラットフォーム間の通信を型安全かつ容易かつ高速にするためのコード生成ツールです。

wifiやBurtoothなど、Flutterでもネイティブの機能にアクセスしたいときが、あると思います。

そして、それは以前、MethodChannelを通じて行われていました。

しかし、MethodChannelは型安全ではありません。

https://pub.dev/packages/pigeon

コーディング手順

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