Things take time

[Flutter] iOS 프로젝트에 여러 개의 FlutterViewController 넣기 및 MethodChannel 구성하기(Swift) 본문

Flutter

[Flutter] iOS 프로젝트에 여러 개의 FlutterViewController 넣기 및 MethodChannel 구성하기(Swift)

겸손할 겸 2022. 5. 25. 13:14

[개요]

이전 글에서 iOS 프로젝트에 플러터 모듈을 import하고 플러터의 뷰 하나를 넣는 테스트를 했다.

https://g-y-e-o-m.tistory.com/186

 

[Flutter] iOS 프로젝트에 FlutterViewController넣기(import Flutter Module to Swift)

[개요] 처음부터 플러터 프로젝트로 만들어서 각각의 네이티브 플랫폼 코드를 사용한다면 플랫폼 채널을 통해 각각의 함수를 호출할 수 있고, FlutterPlatformView를 상속받는 뷰를 넣을수도 있다. 그

g-y-e-o-m.tistory.com

그 글에서도 밝혔지만, 해당 예제는 러프한 소스여서 이 것을 이해한 것을 바탕으로 코드를 작성하려한다.

 

만들고자 하는 목표는

1. 플러터의 뷰 컨트롤러는 하나 이상을 사용할 것이다.

2. 플러터 엔진은 무조건 예열하는 방식으로 사용하며, 직접 선언/호출하는 방법은 지양한다.

3. 플러터 뷰 컨트롤러를 넣는 시점에 method channel도 같이 생성하여, Flutter Module <-> Swift간 통신을 설정해 줄 것이다.

 

이전 글의 소스를 참고/비교해서 보도록하자.

class AppDelegate: FlutterAppDelegate {
    let engines = FlutterEngineGroup(name: "multiple-flutters", project: nil)
    
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }
}
import UIKit
import Flutter
import FlutterPluginRegistrant

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(type:UIButton.ButtonType.custom)
        button.addTarget(self, action: #selector(showFlutterRoute), for: .touchUpInside)
        button.setTitle("Show Flutter!", for: UIControl.State.normal)
        button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
        button.tag = 1
        button.backgroundColor = UIColor.blue
        self.view.addSubview(button)
        
        
        let button2 = UIButton(type:UIButton.ButtonType.custom)
        button2.addTarget(self, action: #selector(showFlutterEntry), for: .touchUpInside)
        button2.tag = 2
        button2.setTitle("Show Flutter!", for: UIControl.State.normal)
        button2.frame = CGRect(x: 80.0, y: 260.0, width: 160.0, height: 40.0)
        button2.backgroundColor = UIColor.red
        self.view.addSubview(button2)
    }
    @objc func showFlutterRoute() {
        let flutterVC = GyeomFlutterViewController(withEntrypoint: nil, route: "/gyeom")
        // let flutterVC = GyeomFlutterViewController(withEntrypoint: "gyeomPage", route: nil)
        self.present(flutterVC, animated: true, completion: nil)
        
        // Storyboard에서 NavigationController import
        // self.navigationController?.pushViewController(flutterVC, animated: false)
    }
    
    @objc func showFlutterEntry() {
        let flutterVC = GyeomFlutterViewController(withEntrypoint: "gyeomEntry", route: nil)
        self.present(flutterVC, animated: true, completion: nil)
        
        // Storyboard에서 NavigationController import
        // self.navigationController?.pushViewController(flutterVC, animated: true)
    }
}

달라진 점

1. firstEngine, secondEngine이 사라짐 => 사실 필요한 플러터의 뷰 개수만큼 생성하면 되겠지만, 비효율적이므로 사용하지 않는다.

2. GeneratedPluginRegistrant.register(with: firstEngine!)을 바로 사용하지 않음 => AppDelegate에서 미리 등록이 아닌 동적 등록을 위함이다.

 

소스를 보면 버튼을 클릭했을 때, GyeomFlutterViewController라는 클래스 객체를 생성하고 해당 객체는 withEntryPoint, route의 옵셔널 파라미터를 받는다. 

 

플러터의 뷰를 가져와 넣을때, route를 사용하는 방법을 이전글에서 설명했다. 그때 설명하지 못했던 방법은 Entry값을 통해 플러터 뷰를 매핑하는 방법인데, 이 후 다트 소스를 참고하면 될것이다.

import Flutter
import FlutterPluginRegistrant
import Foundation

/**
 @interface FlutterViewController
     : UIViewController <FlutterTextureRegistry, FlutterPluginRegistry, UIGestureRecognizerDelegate>
 #else
 @interface FlutterViewController : UIViewController <FlutterTextureRegistry, FlutterPluginRegistry>
 #endif
 */
class GyeomFlutterViewController: FlutterViewController{
    private var flutterEngine: FlutterEngine?
    private var methodChnnel: FlutterMethodChannel?
    
    init(withEntrypoint entryPoint: String?, route: String?) {
        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
        flutterEngine = appDelegate.engines.makeEngine(withEntrypoint: entryPoint, libraryURI: nil, initialRoute: route)
        GeneratedPluginRegistrant.register(with: flutterEngine!)
        super.init(engine: flutterEngine!, nibName: nil, bundle: nil)
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.methodChnnel = FlutterMethodChannel(name: "gyeom-channel", binaryMessenger: self.binaryMessenger)
        // Flutter -> Native Call
        self.methodChnnel?.setMethodCallHandler({
            [weak self]
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            guard let self = self else { return }
            print("GyeomFlutterViewController methodChnnel: \(call.method)")
        })
        // Native -> Flutter Call
        self.methodChnnel?.invokeMethod("swift-to-flutter", arguments: "hello", result: {
            (result) -> Void in
            print("swift-to-flutter result: \(result)")
        })
    }
}

플러터 뷰 컨트롤러를 상속받는 클래스다. 참고로 플러터 뷰 컨트롤러는 UIViewController의 속성을 그대로 가져오면서, 플러터에 대한 추가 프로토콜을 가진다.

 

생성자로 가져온 파라미터를 바탕으로, AppDelegate에 선언한 engines를 가져와 makeEngine을 통해 호출과 동시에 생성, 등록되도록한다. 그리고 뷰가 보여지는 시점에서는 MethodChannel을 등록한다.

 

이렇게 함으로써, 플러터의 뷰컨트롤러를 생성/등록하고 보여질때 메소드 채널까지 등록되도록하는 것이다. 

그럼 ViewController내 버튼을 클릭했을 때, 해당 FlutterViewController객체를 생성하고 present(혹은 push)하여 보여줌과 동시에 메소드채널까지 등록되는 것이다.

 

이제 Flutter의 dart소스를 보면 아래와 같다.

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

@pragma('vm:entry-point')
void gyeomEntry() => runApp(const GyeomEntryPage());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or press Run > Flutter Hot Reload in a Flutter IDE). Notice that the
        // counter didn't reset back to zero; the application is not restarted.
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/',
      routes: {
        '/gyeom': (context) {
          return const GyeomPage();
        },
      },
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

1. @pragma('vm:entry-point'): 네이티브코드를 사용할 것이라고 컴파일에게 알리는 문법의 구문이다.

gyeomEntry()라는 문법이 iOS의 showFlutterEntry()함수내에 있는 withEntryPoint에 들어가는 파라미터와 일치한다는 것을 알 수 있다.

https://github.com/dart-lang/sdk/blob/master/runtime/docs/compiler/aot/entry_point_pragma.md

 

GitHub - dart-lang/sdk: The Dart SDK, including the VM, dart2js, core libraries, and more.

The Dart SDK, including the VM, dart2js, core libraries, and more. - GitHub - dart-lang/sdk: The Dart SDK, including the VM, dart2js, core libraries, and more.

github.com

2. routes: {'/gyeom': ... } : 이전 글에서 설명한 라우팅 방법이다.

 

즉, 플러터 뷰를 띄울 수 있는 2가지의 방법을 모두 소개했고 각각의 GyeomEntryPage()나 GyeomPage()를 구현하면 된다. 단, MethodChannel Name을 맞춰야하는 것을 잊지말자. GyeomPage()의 경우 다음과 같다.

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

class GyeomPage extends StatefulWidget {
  const GyeomPage({Key? key}) : super(key: key);

  @override
  _GyeomPageState createState() => _GyeomPageState();
}

class _GyeomPageState extends State<GyeomPage> {
  static const platform = MethodChannel('gyeom-channel');

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _getBatteryLevel();

    platform.setMethodCallHandler(methodHandelr);
  }

  /**
   * Native -> Flutter
   */
  Future<dynamic> methodHandelr(MethodCall methodCall) async {
    print('methodHandelr: ${methodCall.method}');
    if (methodCall.method == "swift-to-flutter"){
      print('methodHandelr: ${methodCall.arguments}');
      return "received flutter";
    }
  }

  /**
   * Flutter -> Native: 도큐먼트 기본 예제
   */
  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Container(
            child: Text('플러터로 만든 페이지.'),
          ),
        ),
      ),
    );
  }
}

_getBatteryLevel의 경우 MethodChannel 예제로, 공식 도큐먼트에서 사용하는 함수 그대로이고, methodHandler의 경우 Swift -> Flutter로 함수를 호출할 때 사용하기 위해 사용한다.

 

간단히 메소드 채널이름을 분석하면 setMethodHandler를 쓰는 쪽은 메소드를 받기 위해(리턴은 옵셔널)하는것이고 반대로 메소드를 호출하려면 invokeMethod를 사용한다.