Things take time

[Flutter] Flutter와 Native(iOS)간의 통신 방법 - dynamic code 본문

Flutter

[Flutter] Flutter와 Native(iOS)간의 통신 방법 - dynamic code

겸손할 겸 2022. 6. 22. 10:11

기존 글

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

 

[Flutter] Flutter안에 Native(iOS)의 UIView 넣는 방법

[개요] 플러터의 장점은 각 OS별로 짜야하는 UI나 기능 작업들을 하나의 코드인 다트로 작성할 수 있다는 점이다. 다만, 특정한 기능 등은 각 네이티브에서 수행해야할 때가 있다. 마치 하이브리

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

이 글의 경우엔, AppDelegate에서의 역할이 컸다.

 

각각의 플러터 플랫폼 뷰를 상속받는 클래스를 연결하고, 메서드 채널을 연결하는 방식의 기초 방법이었는데 이 방법을 적용하니 문제가 발생되었다. 내 경우엔 플러터에서 네이티브 뷰를 UIKitView로 불러와 사용하지만, 다른 플러터 화면에서도 똑같은 뷰를 재사용해야 했다. 다만 전달하는 파라미터만 달라질 뿐이었기 때문에 생각해볼 건 두 가지였다.

 

1. 사용하는 만큼의 NativeView를 생성한다.

위의 기존 글을 바탕으로 설명한다. GyeomFactory라는 Class를 다른이름으로 생성하는 방법이다. 이런 경우에는 필요한 만큼의 플러터플랫폼 뷰 팩토리를 상속받는 클래스를 계속 생성해야한다. 

-> 즉, 똑같은 뷰를 재사용해야하는데 그만큼을 생성한다는 것 자체가 비효율적이다. 물론 구현은 된다.

 

2. 기존의 GyeomFactory를 재사용한다.

글에서 적은 내용처럼 가장 알맞다. 다만, 이 경우에는 플러터에서 뷰가 스택으로 쌓일 경우 문제가 발생할 수 있다.

 

예를 들어, A라는 라우트(페이지)에서 B라우트를 띄웠을 때(A를 remove하지 않고) A->B라는 라우트가 쌓여있을 것이다. 근데 A에서도 UIKitView를, B에서도 UIKitView를 공통적으로 사용한다고 가정하자. 그리고 각 UIKitView와 플러터 간의 통신을 위해 메서드 채널도 사용해야한다.

 

기본 메서드 채널에 대해 알아보려면, 메서드 채널 튜토리얼을 보면 이해가 될 것이다.

https://flutter-ko.dev/docs/development/platform-integration/platform-channels

 

플랫폼 별 코드 작성

앱에서 커스텀하게 플랫폼 별 코드를 작성하는 방법을 배워보세요.

flutter-ko.dev

튜토리얼에서는 AppDelegate에서 메서드 채널을 생성하여 사용하고 있다.

 

이 튜토리얼과 내가 올린 글을 접합하게 되면, GyeomFactory라는 클래스를 연결하는 작업과(1), 메서드 채널을 생성하는 작업(2)가 각각 하나씩 있는데, 뷰를 재사용하는 입장에서 메서드 채널을 호출 할 경우 AppDelegate에서는 현재 열린게 B인지, A인지는 모른 상태로 함수를 호출한다. 이런 경우, A/B가 갖고 있는 UIKitView 두개가 이벤트를 호출받아 그에 대한 작업을 수행하는 일이 발생해버린다.

 

튜토리얼은 튜토리얼인 만큼 그대로 사용하면 이런 일이 발생한다.

원하는 결과는 다음과 같다.

 

공통적으로 사용하는 네이티브 뷰(GyeomFactory)가 있고, 각 화면에서 파라미터만 다르게 전달할 것이다. 그리고 메서드 채널을 통해 호출할 때는 그 네이티브 뷰는 공통적으로 호출받지 않고 따로 받고싶다.

AppDelegate

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      GeneratedPluginRegistrant.register(with: self)
      
      // Setting CustomUIView
      weak var pluginRegister = self.registrar(forPlugin: "testPlugin")
      let factory = GyeomFactory(messenger: pluginRegister!.messenger())
      // testPlugin 이름으로 등록된 단일 플러그인 Context에 factory와 view-type이란 with-id를 등록
      pluginRegister?.register(factory, withId: "view-type")
      
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

위의 공식 튜토리얼에서 사용한 메서드 채널을 AppDelegate에서 사용하지 않는 것이 중요하다.

GyeomFactory

/**
 FlutterPlatformView를 상속받는 클래스: 실제 플러터 내에 포함될 UIView를 리턴하는 클래스(func view() -> UIView {}가 프로토콜내 필수 구현함수로 구현되어 있다)
 FlutterPlatformViewFactory를 상속받는 클래스 : FlutterPlatformView를 상속받아 UIView를 리턴하는 그 클래스를 연결하는 클래스(create()가 프로토콜내 필수 구현함수로 등록되어 있다, createArgsCodec()는 optional)
 */
class GyeomFactory: NSObject, FlutterPlatformViewFactory {
    private var messenger: FlutterBinaryMessenger?
    private var gyeomView: GyeomView?
    
    override init() {
        super.init()
    }
    
    init(messenger: FlutterBinaryMessenger) {
        super.init()
        self.messenger = messenger
    }
    
    /**
     Only needs to be implemented if `createWithFrame` needs an arguments parameter.
     Dart에서 파라미터 전달시 필요
     */
    func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
        return FlutterStandardMessageCodec.sharedInstance()
    }

    
    func create(
        withFrame frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?
    ) -> FlutterPlatformView {
        let methodChannel = FlutterMethodChannel(name: "method-channel-" + "\(viewId)", binaryMessenger: messenger!)
        gyeomView = GyeomView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            binaryMessenger: messenger,
            methodChannel: methodChannel)
        return gyeomView ?? GyeomView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            binaryMessenger: messenger,
            methodChannel: methodChannel)
    }
}

class GyeomView: NSObject, FlutterPlatformView{
    private var containerView: UIView
    
    init(
        frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?,
        binaryMessenger messenger: FlutterBinaryMessenger?,
        methodChannel: FlutterMethodChannel
    ) {
        containerView = UIView()
        super.init()
        
        methodChannel.setMethodCallHandler({
            [weak self]
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            guard let self = self else { return }
            switch call.method{
            case "mute":
                result(nil)
                break
            case "start":
                result(nil)
                break
            case "stop":
                result(nil)
                break
            default:
                result(FlutterMethodNotImplemented)
                break
            }
        })
    }
    
    func view() -> UIView {
        return containerView
    }
}

"view-type"이란 값으로 UIKitView를 처음 만들 때 호출되는 함수인 create에서 viewId는 고유 값으로 등록된다. 그러므로 이 값을 통해 MethodChannel을 등록, 실제 그려지는 뷰인 GyeomView에 넘겨서 setMethodCallHandler를 통해 등록하면 되는 것이다.

Flutter

UiKitView(
      viewType: 'view-type',
      layoutDirection: TextDirection.ltr,
      creationParams: creationParams,
      creationParamsCodec: const StandardMessageCodec(),
      onPlatformViewCreated: (int id) {
        print('[Gyeom] onPlatformViewCreated call :: id: $id');
        _methodChannel = MethodChannel('method-channel-$id');
      },
),

그리고 onPlatfomViewCreated에서 넘겨받는 result의 id가 viewId이므로, 여기서 플러터단의 메서드 채널을 등록하면 이제 플러터와 네이티브 뷰의 연결이 이루어지게 되며, 이 네이티브뷰를 재사용하더라도 처음 UIKitView를 호출하는 시점에 새로운 viewId가 생성되므로 한번의 호출로 여러개의 네이티브 뷰가 호출받게 되는 불상사는 없을 것이다.