๐ฑ iOS ์ค์
์ด ๋ฌธ์๋ ๋ค์ดํฐ๋ธ iOS ํ๋ก์ ํธ(Swift ๊ธฐ๋ฐ)์ 'Test Solution SDK'๋ฅผ ํตํฉํ๊ธฐ ์ํ ๊ตฌ์ฒด์ ์ธ ์ค์ ๋ฐฉ๋ฒ์ ์๋ดํฉ๋๋ค.
'์์ํ๊ธฐ' ์น์ ์ ์ค์น ๊ฐ์ด๋๋ฅผ ํตํด ํ๋ก์ ํธ์ Flutter Module์ด ์ถ๊ฐ๋์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
FlutterEngine ์ ์ง๊ด๋ฆฌ ํด๋์ค ์์ฑโ
์ฑ์ ์๋ช ์ฃผ๊ธฐ ๋์ ์ ์งํ ๋ณ๋์ ๊ด๋ฆฌ ํด๋์ค๋ฅผ ๋ง๋ญ๋๋ค.
- UIKit (Storyboard)
- SwiftUI
import Foundation
import Flutter
import FlutterPluginRegistrant
class FlutterEngineManager: NSObject, FlutterAppLifeCycleProvider {
static let shared = FlutterEngineManager()
let flutterEngine: FlutterEngine
private let lifecycleDelegate = FlutterPluginAppLifeCycleDelegate()
private override init() {
self.flutterEngine = FlutterEngine(name: "MSCFlutterEngine")
super.init()
flutterEngine.run()
GeneratedPluginRegistrant.register(with: self.flutterEngine)
}
func add(_ delegate: any FlutterApplicationLifeCycleDelegate) {
lifecycleDelegate.add(delegate)
}
}
AppDelegate.swift ํ์ผ์ iOS ์ฑ์ ์๋ช
์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ๊ณ Flutter Engine์ ์ด๊ธฐํํ๋ ํต์ฌ์ ์ธ ์ญํ ์ ํฉ๋๋ค.
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let _ = FlutterEngineManager.shared
return true
}
// ...
}
ViewController.swift ํ์ผ์ ํ
์คํธ๋ฅผ ์์ํ๋ ๋ฒํผ๊ณผ ๊ด๋ จ ์ฝ๋๋ฅผ ์ถ๊ฐํฉ๋๋ค.
import UIKit
import Flutter
class ViewController: UIViewController {
private let CHANNEL: String = "com.mscbrain.sdk.test_solution_sdk/channel"
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type:UIButton.ButtonType.custom)
button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
button.setTitle("ํ
์คํธ ์์", for: UIControl.State.normal)
button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
button.backgroundColor = UIColor.orange
self.view.addSubview(button)
}
@objc func showFlutter() {
let flutterEngine = FlutterEngineManager.shared.flutterEngine
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
flutterViewController.modalPresentationStyle = .fullScreen
let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: flutterViewController.binaryMessenger)
channel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "onResult":
guard let resultMap = call.arguments as? [String: Any],
let status = resultMap["status"] as? String else {
return
}
print("onResult received: \(resultMap)")
if status == "COMPLETED" {
// ํ
์คํธ ์๋ฃ ํ ์ฒ๋ฆฌ ๋ก์ง
} else if status == "ERROR" {
// ์ค๋ฅ ์ฒ๋ฆฌ ๋ก์ง
}
break
case "onInfo":
guard let versionMap = call.arguments as? [String: Any] else { return }
print("TestSolutionSDK_VERSION: \(versionMap)") // ๋ฒ์ ์ ๋ณด ์ถ๋ ฅ
break
case "onLog":
guard let logMap = call.arguments as? [String: Any] else { return }
print("TestSolutionSDK_LOG: \(logMap)") // ๋๋ฒ๊ทธ ๋ก๊ทธ ์ถ๋ ฅ
break
default:
result(FlutterMethodNotImplemented)
}
})
let startData: [String: Any] = [
// ํ์ ๊ฐ
"goodsId": "[GOODS_ID]",
"suid": "[SUID]",
"language": "[LANGUAGE]",
// ์ ํ ๊ฐ
"isDevelopment": true,
"enableHostLogging": true,
"themeColor": 0xFF7E52F3, // ์์: ๋ณด๋ผ์ (AARRGGBB)
"themeMode": "light",
"titleText": "๋์ ์ฑ๊ฒฉ ์ ํ ์ฐพ๊ธฐ"
]
channel.invokeMethod("startTest", arguments: startData)
present(flutterViewController, animated: true, completion: nil)
}
}
์ฃผ์ ํ๋ฆ ์์ฝโ
- ์ฑ ์คํ:
AppDelegate์์ FlutterEngine์ ๋ฏธ๋ฆฌ ์์ฑํ๊ณ ์คํํฉ๋๋ค. SDK ํ๋ฉด์ ์์ฒญํ๊ธฐ ์ ์ ์ด๋ฏธ ์์ง์ด ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋๋์ด ์ค๋น๋ ์ํ๊ฐ ๋ฉ๋๋ค. ์ด๋ก ์ธํด SDK ํ๋ฉด์ด ๋ํ๋๋ ์๋๊ฐ ๋งค์ฐ ๋นจ๋ผ์ง๋๋ค. ๐ - ํต์ ์ฑ๋ ์ค์ : ๋ค์ดํฐ๋ธ์ SDK ๊ฐ์ ํต์ ์ฑ๋์ธ FlutterMethodChannel์ ์ค์ ํ์ฌ SDK๋ก๋ถํฐ
onResult,onLog,onInfo๋ฅผ ์์ ํ ์ค๋น๋ฅผ ํฉ๋๋ค. - ํ
์คํธ ์์:
ViewController์์ ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ๋๋ฅด๋ฉด,channel.invokeMethod("startTest", arguments: startData)์ฝ๋๋ฅผ ํตํด "startTest"๋ผ๋ ์ด๋ฆ์ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด์ startData๋ฅผ SDK๋ก ์ ๋ฌํฉ๋๋ค. - ํ๋ฉด ํ์:
FlutterViewController๋ฅผ ์์ฑํ์ฌ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค๋๋ค. - ๊ฒฐ๊ณผ ์์ : SDK์์
onResult,onInfo,onLog๋ฅผ ํธ์ถํ๊ณ , ์ด ์ ํธ๋ฅผ ๋ฐ์ ๋ค์ดํฐ๋ธ์์ ํ์ ์์ ์ ์ฒ๋ฆฌํฉ๋๋ค.
import Flutter
import FlutterPluginRegistrant
@Observable
class FlutterEngineManager {
let engine: FlutterEngine
init() {
self.engine = FlutterEngine(name: "MSCFlutterEngine")
engine.run()
GeneratedPluginRegistrant.register(with: self.engine)
}
}
import SwiftUI
@main
struct MyApp: App {
@State private var flutterEngineManager = FlutterEngineManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(flutterEngineManager)
}
}
}
FlutterViewController๋ฅผ ๊ฐ์ธ์ SwiftUI ๋ทฐ์ฒ๋ผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ๋ ๋ํผ ๊ตฌ์กฐ์ฒด๋ฅผ ๋ง๋ญ๋๋ค. ์ด ํ์ผ์ Flutter ์ฑ๋ ํต์ ๋ก์ง๊น์ง ํจ๊ป ๋ฃ์ด ๊ด๋ฆฌํ๋ ๊ฒ์ด ํธ๋ฆฌํฉ๋๋ค.
import SwiftUI
import Flutter
struct FlutterView: UIViewControllerRepresentable {
@Environment(FlutterEngineManager.self) private var flutterEngineManager
private let CHANNEL: String = "com.mscbrain.sdk.test_solution_sdk/channel"
func makeUIViewController(context: Context) -> some UIViewController {
let flutterViewController = FlutterViewController(engine: flutterEngineManager.engine, nibName: nil, bundle: nil)
let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: flutterViewController.binaryMessenger)
channel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "onResult":
guard let resultMap = call.arguments as? [String: Any],
let status = resultMap["status"] as? String else {
return
}
print("onResult received: \(resultMap)")
if status == "COMPLETED" {
// ํ
์คํธ ์๋ฃ ํ ์ฒ๋ฆฌ ๋ก์ง
} else if status == "ERROR" {
// ์ค๋ฅ ์ฒ๋ฆฌ ๋ก์ง
}
break
case "onInfo":
guard let versionMap = call.arguments as? [String: Any] else { return }
print("TestSolutionSDK_VERSION: \(versionMap)") // ๋ฒ์ ์ ๋ณด ์ถ๋ ฅ
break
case "onLog":
guard let logMap = call.arguments as? [String: Any] else { return }
print("TestSolutionSDK_LOG: \(logMap)") // ๋๋ฒ๊ทธ ๋ก๊ทธ ์ถ๋ ฅ
break
default:
result(FlutterMethodNotImplemented)
}
})
let startData: [String: Any] = [
// ํ์ ๊ฐ
"goodsId": "[GOODS_ID]",
"suid": "[SUID]",
"language": "[LANGUAGE]",
// ์ ํ ๊ฐ
"isDevelopment": true,
"enableHostLogging": true,
"themeColor": 0xFF7E52F3, // ์์: ๋ณด๋ผ์ (AARRGGBB)
"themeMode": "light",
"titleText": "๋์ ์ฑ๊ฒฉ ์ ํ ์ฐพ๊ธฐ"
]
channel.invokeMethod("startTest", arguments: startData)
return flutterViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
๋ฉ์ธ SwiftUI ๋ทฐ(ContentView)์์ ๋ฒํผ์ ๋ง๋ค๊ณ , ๋ฒํผ์ ํญํ๋ฉด ์์์ ๋ง๋ FlutterView๋ฅผ ์ ์ฒด ํ๋ฉด์ผ๋ก ๋์ฐ๋๋ก ๊ตฌํํฉ๋๋ค.
import SwiftUI
struct ContentView: View {
@State private var isShowingFlutter = false
var body: some View {
VStack {
Button("ํ
์คํธ ์์") {
self.isShowingFlutter = true
}
.padding()
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(8)
}
.fullScreenCover(isPresented: $isShowingFlutter) {
FlutterView()
.ignoresSafeArea()
}
}
}
์ฃผ์ ํ๋ฆ ์์ฝโ
- ์ฑ ์คํ: MyApp์ด ์คํ๋๊ณ
FlutterEngineManager๊ฐ ์์ฑ๋ฉ๋๋ค. ์ด๋ Flutter ์์ง์ด ๋ฏธ๋ฆฌ ์ด๊ธฐํ(pre-warming) ๋ฉ๋๋ค. ๐ - ํต์ ์ฑ๋ ์ค์ : ๋ค์ดํฐ๋ธ์ SDK ๊ฐ์ ํต์ ์ฑ๋์ธ FlutterMethodChannel์ ์ค์ ํ์ฌ SDK๋ก๋ถํฐ
onResult,onLog,onInfo๋ฅผ ์์ ํ ์ค๋น๋ฅผ ํฉ๋๋ค. - ํ
์คํธ ์์:
makeUIViewController๋ด๋ถ์์ ์ฑ๋๊ณผ ํธ๋ค๋ฌ๊ฐ ์ค์ ๋ ์งํ,channel.invokeMethod("startTest", arguments: startData)์ฝ๋๋ฅผ ํตํด "startTest"๋ผ๋ ์ด๋ฆ์ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด์ startData๋ฅผ SDK๋ก ์ ๋ฌํฉ๋๋ค. - ํ๋ฉด ํ์:
FlutterViewController๋ฅผ ์์ฑํ์ฌ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค๋๋ค. - ๊ฒฐ๊ณผ ์์ : SDK๊ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ด๊ฑฐ๋ ๋ก๊ทธ๋ฅผ ์ ๋ฌํ ๋ ๋ฏธ๋ฆฌ ๋ฑ๋กํด๋
setMethodCallHandler๊ฐ ํธ์ถ๋๊ณ , ํธ๋ค๋ฌ ๋ด๋ถ์์ ๋ฉ์๋ ์ด๋ฆ์ ๊ตฌ๋ถํ์ฌ ๊ฐ๊ฐ์ ๋ง๋ ํ์ ์์ ์ ์ฒ๋ฆฌํฉ๋๋ค.