10
7

More than 3 years have passed since last update.

Flutter PlatformView (iOS) の作成

Last updated at Posted at 2019-11-20

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.yamlversion: 0.0.1+1 を書き加えるということをして再度ビルドしたらメッセージが出なくなった。

デフォルトで入れといてくれよ。。。

exampleアプリのバージョンの設定

記事の主旨とは異なるが、これをしないとビルドのたびにアプリを削除しなければならないので、対応しておいたほうがいい。

記事を書いている時点 (2019/11/21) において作成したpluginのexampleアプリは、

  • 1度目のインストールは成功するものの、アプリがインストールされた状態で再インストールしようとすると失敗する
  • (一度アプリを削除してから再インストールすればインストールは成功する)

という問題がある。

exampleにバージョンが設定されていないことが原因で、
https://github.com/flutter/flutter/issues/34477 の記事を参考に example/pubspec.yamlversion: 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でも確認したところ、 やっぱり残っているようだった。

スクリーンショット 2019-11-21 21.12.11.png

MyFirstViewUIView のサブクラスだったけど 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")
  }
}

スクリーンショット 2019-11-21 21.18.01.png

原因は追っていない。がまあいいや。

(2019/2/8追記) Flutter 1.12.13で作ってみた

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7