🦄

Flutter PlatformViewを使ってiOSのネイティブビューを表示する

2020/11/11に公開

はじめに

Flutterを使うと、Android,iOSのスマホ開発が1コードで実現できます。
しかしネイティブなAPIやViewに手が届かないところも存在します。

ただFlutterからネイティブなAPI,Viewを呼び出すことができます。
そのため、基本はFlutterで開発し、手が届かない一部分だけネイティブ開発を併用すれば開発効率と実現できる機能の両立が可能です。

今回はiOSのネイティブViewをFlutter側で表示する方法についてまとめます。

手順

何はともあれ公式ドキュメント
https://flutter.dev/docs/development/platform-integration/platform-views#ios

その他今回参考にした情報ソース
https://qiita.com/kurun_pan/items/7c9c8fba3346ab4c7c5f
https://qiita.com/yoshua/items/a653c6f2256f3619c461
https://www.appbrewery.co/p/flutter-development-bootcamp-with-dart
https://itnext.io/using-native-ui-in-flutter-with-platformview-6b9d46265332

iOSネイティブを使う基本コンセプト

  • Registry:アクティブなpluginを記録を行う
  • Registrar:pluginの登録を行う
  • Plugin:ネイティブなコードで作成されたプラグイン
  • PlatformViewFactory:ネイティブUIを提供するプラグインの一部
  • PlatformView:ネイティブなiOS,AndroidのView
  • Method Channel:FlutterとiOS/Androidでメッセージでやりとりする。すべてのチャネルには、Binary MessengerとCodecが必要
  • Binary Messenger:バイナリのメッセージを使用し、非同期メッセージをやりとりする
  • Codec:DartとKotlin,Swiftの型の変換を行う。

Codec参考
https://flutter.dev/docs/development/platform-integration/platform-channels#codec

処理

以下にこれから出てくるclassと処理内容

  • SwiftMyFirstViewPlugin : Plugin
    • MyFirstFlutterPlatformViewFactoryの呼び出し
    • MyFirstViewをレジストラに登録
  • MyFirstFlutterPlatformViewFactory : PlatformViewFactory
    • メッセージのやり取りで利用。
  • MyFirstView : PlatformView
    • Viewの作成
    • Dart呼び出しで利用

iOS側の処理

Plugin

最初にプラグインを作成します。
プラグインとはFlutterから呼び出すネイティブiOS,Androidのコードです。
そのプラグインでUIを表示するためには、PlatformViewFactoryが必要です。
PlatformViewFactoryを含むネイティブプラグインコードをレジストラに登録します。

MyFirstViewPlugin.swiftを作成。

public class SwiftMyFirstViewPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    registrar.register(MyFirstFlutterPlatformViewFactory(),
                       withId: "my_first_view")
  }
}

上記コードでレジストラに登録した「my_first_view」をFlutter側から呼び出します。
また上記のSwiftMyFirstViewPluginのコードを使うには、AppDelegate.swiftに登録する必要があります。forPluginは任意の名前です。

// AppDelegate.swift
@objc class AppDelegate: FlutterAppDelegate {
 override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
   GeneratedPluginRegistrant.register(with: self)
   SwiftMyFirstViewPlugin.register(with: registrar(forPlugin: "FirstView")!) //add
   return super.application(application, didFinishLaunchingWithOptions: launchOptions)
 }
}

PlatformViewFactory

// MagicViewFactory.swift
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()
  }
}

上記コードは最初のpluginのコードの以下部分で利用している。

registrar.register(MyFirstFlutterPlatformViewFactory(),withId: "my_first_view") 

PlatformView

実際に表示する画面をSwiftで作成します。

class MyFirstView: UIView, FlutterPlatformView {
    
    init(frame: CGRect,arguments: Any?) {
        super.init(frame: frame)
        loadNib()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    func loadNib() {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: "MyFirstViewPlugin", bundle: bundle)
        let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
        self.addSubview(view)
    }
   
   
    func view() -> UIView {
        return self
    }
}

xibファイルの作成

xibファイルを作成します。ファイル名はMyFirstViewPlugin.xibとしてください。
このxibファイルに対して、ボタンや画像、テキストなどをつけて画面を作り、それを呼び出すことができます。
xibの詳細な設定は下記リンクを参考にしてください。
https://qiita.com/uhooi/items/ce1b8f56fe7d3eaca325

MyFirstViewPlugin.swiftファイルにplugin,factory,viewを全てまとめました。
以下コードコピペとxibファイルがあれば動作します。

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 {
    
    init(frame: CGRect,arguments: Any?) {
        super.init(frame: frame)
        loadNib()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    func loadNib() {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: "MyFirstViewPlugin", bundle: bundle)
        let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
        self.addSubview(view)
    }
   
    
    func view() -> UIView {
        return self
    }
}

Dart側の処理

Dart側では、UIKitViewでiOSで準備したViewコードを呼び出すことができます。

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'platformview Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        body: Center(
          child: Container(
            width: 300,
            height: 300,
            child: UiKitView(
              viewType: 'my_first_view',
              creationParams: {"label": "label_text"},
              creationParamsCodec: const StandardMessageCodec(),
            ),
          ),
        ),
      ),
    );
  }
}

最後に

ネイティブビューを利用した際のパフォーマンスについて
https://flutter.dev/docs/development/platform-integration/platform-views#performance

Performance
Platform views in Flutter come with performance trade-offs.
For example, in a typical Flutter app, the Flutter UI is composed on a dedicated raster thread. This allows Flutter apps to be fast, as the main platform thread is rarely blocked.
While a platform view is rendered with Hybrid composition, the Flutter UI is composed from the platform thread, which competes with other tasks like handling OS or plugin messages, etc.
Prior to Android 10, Hybrid composition copies each Flutter frame out of the graphic memory into main memory, and then copies it back to a GPU texture. In Android 10 or above, the graphics memory is copied twice. As this copy happens per frame, the performance of the entire Flutter UI may be impacted.

ネイティブのViewをFlutterから呼び出すとパフォーマンスが落ちる。
そのため、Flutter単体で実現可能であれば極力使わないこと。

Discussion