- Official document: https://docs.flutter.dev/development/add-to-app/ios/project-setup#option-a---embed-with-cocoapods-and-the-flutter-sdk
- Code implementation: https://docs.flutter.dev/add-to-app/ios/add-flutter-screen?tab=engine-uikit-swift-tab
- Complete official sample code: https://github.com/flutter/samples/tree/main/add_to_app/books
1. From the terminal, go to your current iOS native project, for eg: /Users/.../reproduce_issue_ios_native_addtoapp_optionA/
2. Group all current files/directories into a new directory (named MyApp
for eg)
3. Create flutter module and go to module directory
flutter create --template module my_flutter
cd my_flutter
Make sure your existing application and the Flutter module are in sibling directories
➜ reproduce_issue_ios_native_addtoapp_optionA tree -L 2
.
├── MyApp
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ ├── reproduce_issue_ios_native_addtoapp_optionA
│ ├── reproduce_issue_ios_native_addtoapp_optionA.xcodeproj
│ └── reproduce_issue_ios_native_addtoapp_optionA.xcworkspace
└── my_flutter
├── README.md
├── analysis_options.yaml
├── lib
├── my_flutter.iml
├── my_flutter_android.iml
├── pubspec.lock
├── pubspec.yaml
└── test
9 directories, 8 files
4. init Podfile and edit it
cd MyApp
pod init
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
target 'reproduce_issue_ios_native_addtoapp_optionA' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for reproduce_issue_ios_native_addtoapp_optionA
install_all_flutter_pods(flutter_application_path)
end
post_install do |installer|
flutter_post_install(installer) if defined?(flutter_post_install)
end
5. Run pod install in MyApp (Done)
pod install
Result:
➜ MyApp pod install
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/universal-darwin22/rbconfig.rb:21: warning: Insecure world writable dir /Users/huynq/Documents/GitHub/flutter/bin in PATH, mode 040777
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Generating Pods project
Integrating client project
[!] Please close any current Xcode sessions and use `reproduce_issue_ios_native_addtoapp_optionA.xcworkspace` for this project from now on.
Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.
[!] Automatically assigning platform `iOS` with version `16.1` on target `reproduce_issue_ios_native_addtoapp_optionA` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.
[!] Your project does not explicitly specify the CocoaPods master specs repo. Since CDN is now used as the default, you may safely remove it from your repos directory via `pod repo remove master`. To suppress this warning please add `warn_for_unused_master_specs_repo => false` to your Podfile.
6. Open project .xcworkspace
in Xcode (coding part)
- AppDelegate.swift
import UIKit
import Flutter
// The following library connects plugins with iOS platform code to this app.
import FlutterPluginRegistrant
@UIApplicationMain
class AppDelegate: FlutterAppDelegate { // More on the FlutterAppDelegate.
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Runs the default Dart entrypoint with a default Flutter route.
flutterEngine.run();
// Connects plugins with iOS platform code to this app.
GeneratedPluginRegistrant.register(with: self.flutterEngine);
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
}
- ViewController.swift
import UIKit
import Flutter
class ViewController: UIViewController {
@IBOutlet var counterLabel: UILabel!
var methodChannel : FlutterMethodChannel?
var count = 0
override func viewDidLoad() {
super.viewDidLoad()
if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine {
methodChannel = FlutterMethodChannel(name: "dev.flutter.example/counter",
binaryMessenger: flutterEngine.binaryMessenger)
methodChannel?.setMethodCallHandler({ [weak self]
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if let strongSelf = self {
switch(call.method) {
case "incrementCounter":
strongSelf.count += 1
strongSelf.counterLabel.text = "Current counter: \(strongSelf.count)"
strongSelf.reportCounter()
case "requestCounter":
strongSelf.reportCounter()
default:
// Unrecognized method name
print("Unrecognized method name: \(call.method)")
}
}
})
}
}
func reportCounter() {
methodChannel?.invokeMethod("reportCounter", arguments: count)
}
@IBAction func openFlutterButtonClick(_ sender: Any) {
if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine {
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
self.present(flutterViewController, animated: false, completion: nil)
}
}
}
7. Update Flutter module code to communicate to native
- add
provider
package:
flutter pub add provider
- update
main.dart
:
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
/// The entrypoint for the flutter module.
void main() {
// This call ensures the Flutter binding has been set up before creating the
// MethodChannel-based model.
WidgetsFlutterBinding.ensureInitialized();
final model = CounterModel();
runApp(
ChangeNotifierProvider.value(
value: model,
child: const MyApp(),
),
);
}
/// A simple model that uses a [MethodChannel] as the source of truth for the
/// state of a counter.
///
/// Rather than storing app state data within the Flutter module itself (where
/// the native portions of the app can't access it), this module passes messages
/// back to the containing app whenever it needs to increment or retrieve the
/// value of the counter.
class CounterModel extends ChangeNotifier {
CounterModel() {
_channel.setMethodCallHandler(_handleMessage);
_channel.invokeMethod<void>('requestCounter');
}
final _channel = const MethodChannel('dev.flutter.example/counter');
int _count = 0;
int get count => _count;
void increment() {
_channel.invokeMethod<void>('incrementCounter');
}
Future<dynamic> _handleMessage(MethodCall call) async {
if (call.method == 'reportCounter') {
_count = call.arguments as int;
notifyListeners();
}
}
}
/// The "app" displayed by this module.
///
/// It offers two routes, one suitable for displaying as a full screen and
/// another designed to be part of a larger UI.class MyApp extends StatelessWidget {
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Module Title',
theme: ThemeData.light(useMaterial3: true),
routes: {
'/': (context) => const FullScreenView(),
'/mini': (context) => const Contents(),
},
);
}
}
/// Wraps [Contents] in a Material [Scaffold] so it looks correct when displayed
/// full-screen.
class FullScreenView extends StatelessWidget {
const FullScreenView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Full-screen Flutter'),
),
body: const Contents(showExit: true),
);
}
}
/// The actual content displayed by the module.
///
/// This widget displays info about the state of a counter and how much room (in
/// logical pixels) it's been given. It also offers buttons to increment the
/// counter and (optionally) close the Flutter view.
class Contents extends StatelessWidget {
final bool showExit;
const Contents({super.key, this.showExit = false});
@override
Widget build(BuildContext context) {
final mediaInfo = MediaQuery.of(context);
return SizedBox.expand(
child: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
const Positioned.fill(
child: Opacity(
opacity: .25,
child: FittedBox(
fit: BoxFit.cover,
child: FlutterLogo(),
),
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Window is ${mediaInfo.size.width.toStringAsFixed(1)} x '
'${mediaInfo.size.height.toStringAsFixed(1)}',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Consumer<CounterModel>(
builder: (context, model, child) {
return Text(
'Taps: ${model.count}',
style: Theme.of(context).textTheme.headlineSmall,
);
},
),
const SizedBox(height: 16),
Consumer<CounterModel>(
builder: (context, model, child) {
return ElevatedButton(
onPressed: () => model.increment(),
child: const Text('Tap me!'),
);
},
),
if (showExit) ...[
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => SystemNavigator.pop(animated: true),
child: const Text('Exit this screen'),
),
],
],
),
),
],
),
);
}
}
When adding a plugin to Flutter module, need to trigger
pod install
in iOS native project manually: