- Create a normal iOS App (using SwiftUI) called
SmaApp
. - Cd into
SmaApp
. - Run
pod init
andpod install
in theSmaApp
folder to setup Cocoapods (install it withbrew install cocoapods
). - Add
platform :ios, '17.0'
toPodfile
. - Add
Pods
folder to.gitignore
:echo Pods/ >.gitignore
. - Reopen the project using
open SmaApp.xcworkspace
. - Build & run to make sure everything works.
- Run
flutter --template module sma_flutter
to setup Flutter. - Go into
sma_flutter
and runflutter run
(choose iOS if asked for a device). - Modify
Podfile
according to the documentation, but using./sma_flutter
as the path because of how I setup everything.platform :ios, '17.0' flutter_application_path = './sma_flutter' load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') target 'SmaApp' do use_frameworks! install_all_flutter_pods(flutter_application_path) end post_install do |installer| flutter_post_install(installer) if defined? (flutter_post_install) end
- Close the project in Xcode, then run
pod install
andopen SmaApp.xcworkspace
. - IMPORTANT: Go to Project > Build Settings and set "User Script Sandboxing" to
NO
. - Also add a key "Bounjour services" to the project's
Info.plist
as an array with_dartVmService._tcp
as its only string element. - Setup
FlutterEngine
inSmaAppApp.swift
according to documentation. Create aFlutterDependencies
singleton that will be setup as an environmental objectimport SwiftUI import Flutter import FlutterPluginRegistrant class FlutterDependencies: ObservableObject { let engine = FlutterEngine() func init() { engine.run() GeneratedPluginRegistrant.register(with: engine); } } @main struct SmaAppApp: App { @StateObject var flutterDependencies = FlutterDependencies() var body: some Scene { WindowGroup { ContentView() .environmentObject(flutterDependencies) } } }
- Create a
FlutterView
as aUIViewRepresentable
.struct FlutterView: UIViewRepresentable { @EnvironmentObject var flutterDependencies: FlutterDependencies func makeUIView(context: Context) -> some UIView { FlutterViewController( engine: flutterDependencies.engine, nibName: nil, bundle: nil, ).view } func updateUIView(_ uiView: UIViewType, context: Context) { } }
- Use the new view in
ContentView.swift
:struct ContentView { var body: some View { NavigationStack { NavigationLink(destination: {FlutterView()}) { Text("Flutter") } } } }
- Instead of creating a singleton engine, setup an engine group and create new engines in
FlutterView
if you need them.class FlutterDependencies: ObservableObject { let engineGroup = FlutterEngineGroup(name: "", project: nil) var engines: [String: FlutterEngine] = [:] func makeEngine(_ key: String) -> FlutterEngine { if let engine = engines[key] { return engine } let engine = engineGroup.makeEngine(with: nil) GeneratedPluginRegistrant.register(with: engine) engines[key] = engine return engine } }
- Use a
key
to reuse or now an engine:struct FlutterView: UIViewRepresentable { var key: String @EnvironmentObject var flutterDependencies: FlutterDependencies func makeCoordinator() -> FlutterViewController { return FlutterViewController( engine: flutterDependencies.makeEngine(key), nibName: nil, bundle: nil ) } func makeUIView(context: Context) -> some UIView { content.coordinator.view } func updateUIView(_ uiView: UIViewType, context: Context) { } }
- Add multiple
FlutterView
widgets and observe that their counters are independent of each other. - Here's a new view that is bound to some SwiftUI state. It sets up a method channel to notify flutter about state changes and to receive state changes from the Flutter side.
struct FV: UIViewRepresentable { var key: String @Binding var count: Int @EnvironmentObject var flutterDependencies: FlutterDependencies func makeCoordinator() -> (FlutterViewController, FlutterMethodChannel) { let engine = flutterDependencies.makeEngine(key) let channel = FlutterMethodChannel( name: "sma/counter", binaryMessenger: engine.binaryMessenger) channel.setMethodCallHandler { call, result in count = call.arguments! as! Int result(nil) } channel.invokeMethod("set", arguments: count) let vc = FlutterViewController( engine: engine, nibName: nil, bundle: nil) return (vc, channel) } func makeUIView(context: Context) -> some UIView { context.coordinator.0.view } func updateUIView(_ uiView: UIViewType, context: Context) { context.coordinator.1.invokeMethod("set", arguments: count) } }
- Here's a counter on the Flutter side. It sets up a method channel to receive state changes from Swift and to notify Swift about a changed state (hoping that
notifiyListeners
is only called if something is actually changed). Theincrement
method is for easy of use within the typical counter widget.class Counter extends ValueNotifier { Counter() : super(0); final channel = const MethodChannel('sma/counter') ..setMethodCallHandler((call) async { counter.value = call.arguments as int; }); void increment() { counter.value++; } @override void notifyListeners() { super.notifyListeners(); unawaited(channel.invokeMethod('set', value)); } }
Created
April 8, 2024 08:55
-
-
Save sma/4a75a0b4861ab7a11715713364601ee8 to your computer and use it in GitHub Desktop.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment