Search code examples
swiftflutter

Open iOS native screen from Flutter application hiding flutter BottomNavigationBar


I have create a Flutter application which has BottomNavigationBar. Suppose this bottom bar has 3 BottomNavigationBarItem. 2 of them flutter screen but 3rd screen I want to launch as iOS native screen. I can do by passing the the method channel on iOS native code to launch ViewController. But iOS native screen hiding flutter bottom tab bar. I want it should place under the bottom bar so that I can switch in between all other tabs.

Flutter Code:

import 'package:flutter/material.dart';
import 'package:flutter/services.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: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: BottomNavigationBarExample(),
    );
  }
}

class BottomNavigationBarExample extends StatefulWidget {
  const BottomNavigationBarExample({super.key});

  @override
  State<BottomNavigationBarExample> createState() =>
      _BottomNavigationBarExampleState();
}

class _BottomNavigationBarExampleState
    extends State<BottomNavigationBarExample> {
  int _selectedIndex = 0;
  static const TextStyle optionStyle =
      TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
  static const List<Widget> _widgetOptions = <Widget>[
    Text(
      'Index 0: Home',
      style: optionStyle,
    ),
    BusinessClass(),
    
    MoreClass()
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BottomNavigationBar Sample'),
      ),
      body: Center(
        child: _widgetOptions.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(

        type: BottomNavigationBarType.fixed, // This is all you need!
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Business',
          ),         
          BottomNavigationBarItem(
            icon: Icon(Icons.more_horiz),
            label: 'More',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

class MoreClass extends StatefulWidget {
  const MoreClass({super.key});

  @override
  State<MoreClass> createState() => _MoreClassState();
}

class _MoreClassState extends State<MoreClass> {
  static const platform = MethodChannel('com.example.flutter_ios_channel');

  @override
  void initState() {
    // TODO: implement initState

    openNativeScreen();
    super.initState();
  }

  void openNativeScreen() async {
    try {
      await platform.invokeMethod('openNativeScreen');
    } catch (e) {
      print('Error opening native screen: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 400,
      color: Colors.green,
    );
  }
}

Now iOS Native Code in AppDelegate


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

      let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
      let channel = FlutterMethodChannel(name: "com.example.flutter_ios_channel", binaryMessenger: controller.binaryMessenger)

          channel.setMethodCallHandler({
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            // Handle method calls from Flutter here
            if call.method == "openNativeScreen" {
              // Replace with code to open your native iOS screen

                let storyboard = UIStoryboard(name: "Main", bundle: nil)
                let viewController = storyboard.instantiateViewController(withIdentifier: "ViewC") as! MyViewController

                self.window.rootViewController = viewController
                self.window.makeKeyAndVisible()
                
              result(nil) // Optional: Sending a result back to Flutter
            } else {
              result(FlutterMethodNotImplemented)
            }
          })

      
      GeneratedPluginRegistrant.register(with: self)
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)      
  }


Solution

  • You need to embed the native screen within the Flutter app rather than replacing the root view controller. This can be done by using a FlutterViewController and presenting it on top of the current Flutter view. You can try this:

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
    
          let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
          let channel = FlutterMethodChannel(name: "com.example.flutter_ios_channel", binaryMessenger: controller.binaryMessenger)
    
          channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if call.method == "openNativeScreen" {
              let storyboard = UIStoryboard(name: "Main", bundle: nil)
              let viewController = storyboard.instantiateViewController(withIdentifier: "ViewC") as! MyViewController
    
              // Present the view controller modally
              controller.present(viewController, animated: true, completion: nil)
              
              result(nil)
            } else {
              result(FlutterMethodNotImplemented)
            }
          }
    
          GeneratedPluginRegistrant.register(with: self)
          return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
    

    Update: Ok, than you can use PlatformView.

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class NativeView extends StatelessWidget {
      const NativeView({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        // This is used in the platform side to register the view.
        const String viewType = 'native-view';
        // Pass parameters to the platform side.
        final Map<String, dynamic> creationParams = <String, dynamic>{};
    
        return Platform.isIOS
            ? UiKitView(
                viewType: viewType,
                layoutDirection: TextDirection.ltr,
                creationParams: creationParams,
                creationParamsCodec: const StandardMessageCodec(),
              )
            : Text('This platform is not supported');
      }
    }
    

    You need to register platform view right before your app initializes.

    void main() {
      if (Platform.isIOS) {
        // Register the platform view for iOS
        final Map<String, dynamic> creationParams = <String, dynamic>{};
        // This uses the `uiKitView` for iOS.
        UiKitView(viewType: 'native-view', creationParams: creationParams, creationParamsCodec: const StandardMessageCodec());
      }
      runApp(const MyApp());
    }
    

    Your NativeView class will look like this:

    import Flutter
    import UIKit
    
    class NativeViewFactory: NSObject, FlutterPlatformViewFactory {
        private var messenger: FlutterBinaryMessenger
    
        init(messenger: FlutterBinaryMessenger) {
            self.messenger = messenger
            super.init()
        }
    
        func create(
            withFrame frame: CGRect,
            viewIdentifier viewId: Int64,
            arguments args: Any?
        ) -> FlutterPlatformView {
            return NativeView(
                frame: frame,
                viewIdentifier: viewId,
                arguments: args,
                binaryMessenger: messenger)
        }
    }
    
    class NativeView: NSObject, FlutterPlatformView {
        private var _view: UIView
    
        init(
            frame: CGRect,
            viewIdentifier viewId: Int64,
            arguments args: Any?,
            binaryMessenger messenger: FlutterBinaryMessenger?
        ) {
            _view = UIView()
            super.init()
            createNativeView(view: _view)
        }
    
        func view() -> UIView {
            return _view
        }
    
        private func createNativeView(view _view: UIView) {
            // Customize your native view here
            _view.backgroundColor = UIColor.red
    
            let label = UILabel()
            label.text = "Native iOS View"
            label.textAlignment = .center
            label.frame = _view.bounds
            label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            _view.addSubview(label)
        }
    }
    

    And here is your AppDelegate:

    import UIKit
    import Flutter
    
    @UIApplicationMain
    @objc class AppDelegate: FlutterAppDelegate {
      override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        let factory = NativeViewFactory(messenger: controller.binaryMessenger)
        registrar(forPlugin: "NativeView")?.register(factory, withId: "native-view")
    
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
    }