본문 바로가기

개발 코딩 정보 공유/플루터 Flutter

ios 네이티브 앱에 flutter 사용하기

네이티브 ios 에 flutter 적용하기

소개

안녕하세요. 김과자 입니다.

이전에 소개 해드린 안드로이드 flutter 모듈 사용에 관하여 참고 하시고

ios 도 오늘 소개 해드리는 방법으로 테스트 해보시기 바랍니다.

 

본문

안드로이드와 마찬가지로 모듈을 만들어줍니다.

방법은 아래에

↓↓↓

2023.07.01 - [개발 코딩 정보 공유/플루터 Flutter] - 안드로이드 네이티브앱에 flutter 소스 사용하기

 

module과 네이티브 소스는 아래와 같이 셋팅합니다. (제 기준)

 

[최상위폴더]

-> [module]

-> [native project]

 

우선 기존의 소스에 pod을 셋팅해야 합니다.

pod 셋팅이 되어있다는 가정하에…

pod파일에 아래의 코드를 추가합니다.

 

platform :ios, '14.0'

# 이부분 추가
#flutter_application_path = '../ios_ui_module'
#load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'my project' do
  use_frameworks!
  #pod 'Alamofire'
  #pod 'AlamofireImage', '~> 4.1'
  pod 'SwiftyJSON'
  pod 'SnapKit', '~> 5.6.0'
...

# 이부분 추가
#install_all_flutter_pods(flutter_application_path)
end

#이부분 추가
#post_install do |installer|
#  flutter_post_install(installer) if defined?(flutter_post_install)
#end

 

이렇게 pod 파일에 추가하시고 아래의 커맨드를 입력합니다.

 

> pod install

 

만약에 잘 안되시면 pod.lock 을 날리고, pod 폴더도 통째로 날린후 (삭제후) 다시 pod install 진행합니다.

그리고 나서 native 소스를 열어줍니다. 
appdelegate 에서 아래의 코드를 추가하기…
전에 확인할게 있습니다. 모듈 폴더로 가셔서 ios 코드를 살펴 봐야 합니다. 
그런데 아무리 봐도 생성된 소스가 없습니다.

 

???

 

없다 없어...

당황하지 마시고... 숨겨진 파일 보기를 하셔서…

 

👉맥에서 숨겨진 파일 폴더 보기 단축키 (모든 파일 보기) :  cmd + shift + . 

 

이제야 파일이 보입니다.

ios 폴더로 진입하셔서 프로젝트를 열어줍니다.

signing 설정을 해줍시다… 자신의 계정으로 맞춰줍니다.

 

Signing 은 알아서 설정하자...

 

flutter 설정

그리고 다시 네이티브 소스를 열어보겠습니다.

appdelegate 로 오셔서

 

import Flutter
import FlutterPluginRegistrant
...

class AppDelegate: FlutterAppDelegate{

    //flutter
    let engineGroup = FlutterEngineGroup(name: "flutter_test_group", project: nil)
    var customEngine1: FlutterEngine?

...


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

...
//flutter
        customEngine1 = engineGroup.makeEngine(withEntrypoint: nil,
                                               libraryURI: nil,
                                               initialRoute: "/init_page")
        GeneratedPluginRegistrant.register(with: customEngine1!)
...

*FlutterEngineGroup name 부분과 initialRoute 부분은 마음대로 하시고 알고는 계셔야 합니다.

 

 

 

native 소스 flutter 사용단

import Flutter

저는 화면에 진입하고 flutter와 통신하는 소스를 추가하였습니다.

적당히 viewDidLoad 쯤에 작성 하면 되겠습니다.

dart 와 통신하는 부분입니다.

 

//flutter
if let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).customEngine1{ //아까 appdelegate 에 추가한...
    let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
    let newsChannel2 = FlutterMethodChannel(name:channelName2, binaryMessenger: flutterViewController.binaryMessenger)

    newsChannel2.setMethodCallHandler({
        [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in

        if(call.method == "getCurrentPageData"){ //메서드는 dart 와 맞춰주어야 합니다.
            var dataDic = [:]
            if let currentData = MapDataModule.shared.nextData{
                dataDic["title"] = "\(currentData.title ?? "")"
                if let desc = currentData.desc{
                    if(desc.count > 0){
                        dataDic["desc1"] = desc[0]
                        if(desc.count > 1){
                            dataDic["desc2"] = desc[1]
                        }

                    }
                }
                dataDic["vcId"] = currentData.id!
                dataDic["imgMain"] = currentData.icon ?? ""
            }

            result(dataDic)
        }
    })
}
let channelName2 = "com.abc.module/test_channel2"
let channelName = "com.abc.module/test_channel"

적당한 채널명도 설정합니다. dart 소스와 맞추어야 합니다.

마지막으로 페이지 이동하는 코드도 작성하겠습니다.

//대략 버튼 클릭시 실행...
getFlutterInitPage()
...

 

//flutter page move...
func getFlutterInitPage(){
    if let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).customEngine1{
        let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
        let newsChannel = FlutterMethodChannel(name:channelName, binaryMessenger: flutterViewController.binaryMessenger)

        newsChannel.invokeMethod("getInitPage", arguments: "0", result: {
                   (result) -> Void in
            print("swift->flutter: \(String(describing: result))")
        })

        flutterViewController.modalPresentationStyle = .overCurrentContext
        flutterViewController.isViewOpaque = false
        self.present(flutterViewController, animated: false, completion: nil)
      }
}

채널을 두개 설정한 이유는 하나는 보내고 하나는 받는 채널로 테스트 하기 위함 입니다.

 

flutter dart 소스

main.dart

route를 사용해도 되지만 이번에는 사용하지 않습니다.

import 'package:f_module_test/InfoPage.dart';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // routes: {
      //   '/singleInputAct': (context) => const SingleInputPage(title: '111111'),
      //   '/multiButtonAct': (context) => const MultiButtonPage(title: '222222'),
      //   '/InfoAct': (context) => const InfoPage(title: '333333'),
      // },
      home: const InfoPage(title: ''),
    );
  }
}

 

InfoPage.dart

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

import 'Const.dart';

class InfoPage extends StatefulWidget {
  const InfoPage({super.key, required this.title});

  final String title;

  @override
  State<InfoPage> createState() => _InfoPageState();
}

class _InfoPageState extends State<InfoPage> {
  var _param = "empty";

  @override
  void initState() {
    super.initState();

		//채널명은 네이티브 소스와 맞추어야 합니다.
    methodChannel.setMethodCallHandler(methodHandler);
  }

/**
	Flutter -> native
	flutter 화면에서 native 데이터를 요청할 경우
*/
  Future<String?> getCurrentPageData() async {
    final dataMap =
        await methodChannel2.invokeMethod<Map>('getCurrentPageData');
		//채널명을 맞추셔야 합니다.
    print(dataMap.toString());

    setState(() {
      _param = dataMap.toString();
    });

    return "success";
  }

//처음 화면에서 진입했을경우
  Future<dynamic> methodHandler(MethodCall methodCall) async {
    print('methodHandler: ${methodCall.method}');
    if (methodCall.method == "getInitPage") { //네이티브에서 맞춘 메서드명
      print('methodHandler: ${methodCall.arguments}');

      //계산...
      //String result = getAssessResult(methodCall.arguments);
      setState(() {
        _param = methodCall.arguments;
      });
      return "received flutter";
    }
  }

  String getAssessResult(String arguments) {
    return "aaaaaaaaaaa";
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              '안녕하세요. 입력을 시작합니다.',
            ),
            const Image(
              image: AssetImage('assets/ic_intro.png'),
              width: 200,
              height: 100,
              fit: BoxFit.fill,
            ),
            Text(
              _param,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            TextButton(
                onPressed: () {
                  //채널을 통해 메서드 호출
                  getCurrentPageData();
                },
                child: Text("데이터가져오기"))
          ],
        ),
      ),
    );
  }
}

 

채널도 작성합니다.

 

import 'package:flutter/services.dart';

const channelName1 = 'com.abc.module/test_channel';
const channelName2 = 'com.abc.module/test_channel2';
const methodChannel = MethodChannel(channelName1);
const methodChannel2 = MethodChannel(channelName2);

 

 

결론

이렇게 해서 native 기반 flutter 와 화면 / 데이터를 주고 받는 소스를 작성해 보았습니다.

실행하면 native소스에서 설정한 channel을 통해 invokeMethod로 flutter 소스를 호출합니다.

handler 를 통해 _param 을 받고 화면에 text 로 표시해줍니다.

데이터 가져오기 버튼을 클릭하면 channel2 를 통해 native 소스의 handler 로 전달 됩니다.

끝.