FlutterでSwiftUIを呼び出す

# はじめに
SwiftUIの勉強をしようと思ったのですが、SwiftUIのプロジェクトを作成して勉強進めるより、馴染みのあるFlutterからSwiftUIを呼び出して利用する方が勉強しやすいかなと思い、FlutterからSwiftUIを呼び出す方法を調査しました。

# 呼び出し方
## Flutter側
ネイティブ側の処理を呼び出すコードです。
ボタンのイベントなどから呼び出してください。

```dart
import 'package:flutter/services.dart';
Future<void> _showSwiftUIView() async {
  try {
    // ChatGPTに聞いたところ、Channel名には、
    // パッケージ名＋用途を表す名前を指定するのが推奨されるようです
    const MethodChannel channel = MethodChannel("com.example.app/view");
    await channel.invokeMethod('showSwiftUIView');
  } on PlatformException catch (e) {
    print("Failed to invoke method: '${e.message}'.");
  }
}
```

## iOS側

### ネイティブ側のメソッドを作成
Flutter側から呼び出すiOS側の処理を実装します。

```diff_swift:ios/Runner/AppDelegate.swift
import Flutter
import UIKit
+import SwiftUI

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

+   let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
+   // nameはFlutter側と合わせる必要がある  
+   let methodChannel = FlutterMethodChannel(name: "com.example.app/view", binaryMessenger: controller as! FlutterBinaryMessenger)
+    methodChannel.setMethodCallHandler({
+     (call: FlutterMethodCall, result: FlutterResult) -> Void in
+       switch call.method {
+         case "showSwiftUIView" :
+           // SwiftUIで実装した画面を呼び出す
+           let swiftUIView = SampleView()
+           let hostingController = UIHostingController(rootView: swiftUIView)
+           controller.present(hostingController, animated: true, completion: nil)
+         default :
+           result(nil)
+       }
+   })
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
```

### SwiftUIでUIを作成
Flutter側から呼び出し表示するUIを作成します。

```swift:ios/Runner/SampleView.swift
import SwiftUI

struct SampleView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

#Preview {
    SampleView()
}
```

# 表示
![view1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/60355499-398f-4e55-835b-344338fbfda4.gif)


# その他の表示
モーダル表示以外にも画面遷移が可能です
## 画面遷移
```diff_swift:ios/Runner/AppDelegate.swift
import Flutter
import UIKit
+import SwiftUI

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
+   // ナビゲーションようにコントローラーを作成
+   let navigationController = UINavigationController(rootViewController: controller)
+   // 透明にしておかないと、Flutter側の画面のAppBar上部が白色となる
+   navigationController.navigationBar.isTranslucent = true
+   navigationController.navigationBar.setBackgroundImage(UIImage(), for: .default)
+   navigationController.navigationBar.shadowImage = UIImage()

+   window = UIWindow(frame: UIScreen.main.bounds)
+   window?.rootViewController = navigationController
+   window?.makeKeyAndVisible()


    
    // nameはFlutter側と合わせる必要がある  
    let methodChannel = FlutterMethodChannel(name: "com.example.app/view", binaryMessenger: controller as! FlutterBinaryMessenger)
     methodChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: FlutterResult) -> Void in
        switch call.method {
          case "showSwiftUIView" :
            // SwiftUIで実装した画面を呼び出す
            let swiftUIView = SampleView()
            let hostingController = UIHostingController(rootView: swiftUIView)
-           controller.present(hostingController, animated: true, completion: nil)
+           controller.navigationController?.pushViewController(hostingController, animated: true)
          default :
            result(nil)
        }
    })
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
```