Skip to content

Instantly share code, notes, and snippets.

@sma
Created April 8, 2024 08:55
Show Gist options
  • Save sma/4a75a0b4861ab7a11715713364601ee8 to your computer and use it in GitHub Desktop.
Save sma/4a75a0b4861ab7a11715713364601ee8 to your computer and use it in GitHub Desktop.

Add Flutter to an iOS Appplication

  1. Create a normal iOS App (using SwiftUI) called SmaApp.
  2. Cd into SmaApp.
  3. Run pod init and pod install in the SmaApp folder to setup Cocoapods (install it with brew install cocoapods).
  4. Add platform :ios, '17.0' to Podfile.
  5. Add Pods folder to .gitignore: echo Pods/ >.gitignore.
  6. Reopen the project using open SmaApp.xcworkspace.
  7. Build & run to make sure everything works.
  8. Run flutter --template module sma_flutter to setup Flutter.
  9. Go into sma_flutter and run flutter run (choose iOS if asked for a device).
  10. 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
    
  11. Close the project in Xcode, then run pod install and open SmaApp.xcworkspace.
  12. IMPORTANT: Go to Project > Build Settings and set "User Script Sandboxing" to NO.
  13. Also add a key "Bounjour services" to the project's Info.plist as an array with _dartVmService._tcp as its only string element.
  14. Setup FlutterEngine in SmaAppApp.swift according to documentation. Create a FlutterDependencies singleton that will be setup as an environmental object
    import 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)
            }
        }
    }
  15. Create a FlutterView as a UIViewRepresentable.
    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) {
        }
    }
  16. Use the new view in ContentView.swift:
    struct ContentView {
        var body: some View {
            NavigationStack {
                NavigationLink(destination: {FlutterView()}) {
                    Text("Flutter")
                }
            }
        }
    }
  17. 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
        }
    }
  18. 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) {
        }
    }
  19. Add multiple FlutterView widgets and observe that their counters are independent of each other.
  20. 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)
        }
    }    
  21. 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). The increment 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));
      }
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment