FlutterでiOSのネイティブコードを呼び出す

Flutterで[MethodChannel](https://docs.flutter.io/flutter/services/MethodChannel-class.html)を使うとAndroid/iOSのネイティブコードとやりとりができます。Androidはプログラミング経験があるのでMethodChannelもすんなり理解できましたが、iOSは全然経験がなかったので、とりあえず入門書を読んだりしつつHello World的なことをしてみました。

今更Objective-Cは触りたくないのでSwiftでやりました。Kotlinと書き方似てるから親しみやすい。
FlutterのプロジェクトでiOSのネイティブ処理をSwiftで行うためには、プロジェクト作成時に以下のように指定します。

```
flutter create -i swift my_app
```

以下のようなコードでDartからSwiftのコードを呼び出し、結果を返してもらうことができました。iOSの方でいじる必要があったのは**AppDelegate.swiftだけ**です。

```lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _channel = MethodChannel('hello_ios');
  String _message = 'Push Button';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hello iOS'),
      ),
      body: Center(
        child: Text(_message),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          final message = await _channel.invokeMethod('getMessage');
          setState(() {
            _message = message;
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
```

```ios/Runner/AppDelegate.swift
import UIKit
import Flutter

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

    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let methodChannel = FlutterMethodChannel(name: "hello_ios", binaryMessenger: controller)

    methodChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: FlutterResult) -> Void in

      if call.method == "getMessage" {
        result("Hello from iOS")
      } else {
        result(FlutterMethodNotImplemented)
      }
    })
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
```

AndroidではFlutterActivityとFlutterViewが存在していましたが、iOSではFlutterViewControllerが両方を兼ね備えたようなクラスっぽいです。あとはMethodChannelを使う方法も同じ。結果の返し方がちょっとAndroidと違いますが、たぶんよりSwiftっぽい書き方になっているのだと思います。渡せる値の種類とかは同じです。

受け渡し可能なデータ型は以下のとおりです。

| Dart | Android | iOS
| --- | --- | ---
| null | null | nil (NSNull when nested)
| bool | java.lang.Boolean | NSNumber numberWithBool:
| int | java.lang.Integer | NSNumber numberWithInt:
| int, if 32 bits not enough | java.lang.Long | NSNumber numberWithLong:
| double | java.lang.Double | NSNumber numberWithDouble:
| String | java.lang.String | NSString
| Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes:
| Int32List | int[] | FlutterStandardTypedData typedDataWithInt32:
| Int64List | long[] | FlutterStandardTypedData typedDataWithInt64:
| Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64:
| List | java.util.ArrayList | NSArray
| Map | java.util.HashMap | NSDictionary

FlutterではUIの部分はAndroidと完全に共通のコードでDartで実装できるため、Storyboardとか使う必要はありません。SwiftのAPIの呼び出し方さえわかれば、AppDelegateの中にちょっと書くだけで簡単にネイティブ処理が書けそうだということがわかりました。

VSCodeでのSwiftのコード補完さえしっかりできるようになれば、XCodeのエディターを使わなくともVSCodeだけでiOSのネイティブ処理が作れそうな気がする。素晴らしい。

Flutterはいいぞ。