FlutterでiOSのPlatformView (UiKitView) を作るまでに手こずったのでメモしておく。
前提
- 対象とするiOSのバージョンは11.0以上
- Swift
- Xcode 10.3 (Xcode 11入れていなかった。。)
- できるだけPlatformViewにだけフォーカスしたコードにする
- View作成時にパラメータを指定して、それを表示するだけ
- Androidは追って
iOS, Swiftは初心者なので、生暖かい目が嬉しいです。
プロジェクトの作成と設定
作成
https://flutter.dev/docs/development/packages-and-plugins/developing-packages を参考にプロジェクトを作る。
$ flutter create --org org.yyyyyyyy --template=plugin -i swift -a kotlin my_first_view
$ cd my_first_view
$ flutter pub get
$ cd example
$ flutter build ios --no-codesign
対象とするiOSのバージョンの変更
そのままでは対象とするiOSのバージョンが8.0以上となっているので11.0にする為に以下の変更を行う。
ios/my_first_view.podspec
- s.ios.deployment_target を 11.0 に変更
- ※exampleで試すだけならいらないかもしれない
example/ios/Runner.xcodeproj/project.pbxproj
- IPHONEOS_DEPLOYMENT_TARGET を 11.0 に変更
PlatformViewを使うための設定
example/ios/Runner/Info.plist
に以下を加える。
<key>io.flutter.embedded_views_preview</key>
<true/>
これは https://api.flutter.dev/flutter/widgets/UiKitView-class.html に書いてました。
Embedding UIViews is still in release preview, to enable the preview for an iOS app add a boolean field with the key 'io.flutter.embedded_views_preview' and the value set to 'YES' to the application's Info.plist file.
ビルドしてみる
以上が済んだら、一度ビルドし直して、exampleをXcodeで開いて
$ flutter clean
$ cd example
$ flutter build ios --no-codesign
$ open ios/Runner.xcworkspace
ビルドをしてみる。
2019/02/08追記 Flutter 1.12.13の場合
Flutter 1.12.13のhotfixのいくつから、以下のようなメッセージが flutter build
で出るようになった。
Action Required: You must set a build name and number in the pubspec.yaml file version field before submitting to the App Store.
次の「exampleアプリのバージョンの設定」で書いている内容と同じこと言っている気がする。
なので、example/pubspec.yaml
に version: 0.0.1+1
を書き加えるということをして再度ビルドしたらメッセージが出なくなった。
デフォルトで入れといてくれよ。。。
exampleアプリのバージョンの設定
記事の主旨とは異なるが、これをしないとビルドのたびにアプリを削除しなければならないので、対応しておいたほうがいい。
記事を書いている時点 (2019/11/21) において作成したpluginのexampleアプリは、
- 1度目のインストールは成功するものの、アプリがインストールされた状態で再インストールしようとすると失敗する
- (一度アプリを削除してから再インストールすればインストールは成功する)
という問題がある。
exampleにバージョンが設定されていないことが原因で、
https://github.com/flutter/flutter/issues/34477 の記事を参考に example/pubspec.yaml
に version: 0.0.1+1
を書き加える。
実装
Plugin (Dart)
lib/my_first_view.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
class MyFirstView extends StatelessWidget {
const MyFirstView({
Key key,
this.label,
}) : super(key: key);
final String label;
@override
Widget build(BuildContext context) {
return UiKitView(
viewType: 'my_first_view',
creationParams: <String, dynamic>{
'label': label,
},
creationParamsCodec: StandardMessageCodec(),
);
}
}
Plugin (Swift)
ios/Classes/SwiftMyFirstViewPlugin.swift
import Flutter
import UIKit
public class SwiftMyFirstViewPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
registrar.register(MyFirstFlutterPlatformViewFactory(),
withId: "my_first_view")
}
}
class MyFirstFlutterPlatformViewFactory: NSObject, FlutterPlatformViewFactory {
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
return MyFirstView(frame: frame, arguments:args)
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
class MyFirstView: UIView, FlutterPlatformView {
let label = UILabel()
func view() -> UIView {
return self
}
required init?(coder aDecoder: NSCoder) {
fatalError("not implemented")
}
init(frame: CGRect, arguments: Any?) {
super.init(frame: frame)
backgroundColor = .yellow
addSubview(label)
if let arguments = arguments as? [String:Any?] {
label.text = arguments["label"] as? String ?? "no label"
label.sizeToFit()
}
}
}
example (Dart)
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:my_first_view/my_first_view.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: SizedBox.fromSize(
size: Size(300, 300),
child: MyFirstView(
label: 'helooooooooooooo',
),
),
),
),
);
}
}
結果
できた。
はまった点、気をつけた点
Pluginでのパラメータの受け渡し
Dartから以下の部分でパラメータを渡している。
return UiKitView(
viewType: 'my_first_view',
creationParams: <String, dynamic>{
'label': label,
},
creationParamsCodec: StandardMessageCodec(),
);
https://api.flutter.dev/flutter/widgets/UiKitView-class.html のうち creationParamsCodec
のDocをみるとわかるのだけど、 creationParams
を指定する際には creationParamsCodec
の指定が必須、とのこと。
さらに creationParamsCodec
のDocにPlatform側でも -[FlutterPlatformViewFactory createArgsCodec:]
で同じcodecを指定する必要がある旨が書いてあるので、Swiftでは
class MyFirstFlutterPlatformViewFactory: NSObject, FlutterPlatformViewFactory {
func create(...) -> FlutterPlatformView {
...
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
のように createArgsCodec
で指定している。
ちなみに同じcodecを指定する必要があるとあったので FlutterStandardMessageCodec()
を指定してて「うまくいかねー」とか思って悩んでてググったら sharedInstance で取得したインスタンスを使うようであることがわかった。
同じだけだとダメなのね。Docだけでわからなかった。。
SubViewのサイズの変更
今回作成した MyFirstView
(FlutterPlatformView のサブクラス)に含めたSubViewのサイズや位置をどのタイミングで指定したらいいかわからない。
AutoLayoutで頑張るしかないのかな。
開放されない (deinitが呼ばれない)
exampleアプリでページ遷移できるようにし、 MyFirstView
がリリースされているか確認したけど、呼ばれない。Memory Graphでも確認したところ、 やっぱり残っているようだった。
MyFirstView
を UIView
のサブクラスだったけど NSObject
にしてみたところ、deinitが呼び出され、Memory Graphをみると開放されているようだった。
class MyFirstView: NSObject, FlutterPlatformView {
let mainView = UIView()
func view() -> UIView {
return mainView
}
init(frame: CGRect, arguments: Any?) {
super.init()
mainView.frame = frame
// 省略
}
deinit {
print("enter deinit")
}
}
原因は追っていない。がまあいいや。