How can I "push" a UIViewController from FlutterViewController

Viewed 12820

I want to "push" a new native view (Activity as in Android & UIViewController as in iOS) upon our Flutter view, and after that new native view is completed/finished, make screen come back to our Flutter view.

I can do this in Android. But I'm very new to iOS, when I try to do this in ios/Runner/AppDelegate.m:

  SwiftViewController *swiftViewController = [controller.storyboard instantiateViewControllerWithIdentifier:@"SwiftViewController"];
  [(FlutterViewController *)self.window.rootViewController pushViewController:swiftViewController animated:YES];

It gives error:

No visible @interface for "FlutterViewController" declared the selector 'pushViewController:animated'

So how to do it in iOS? Thanks

6 Answers

In Swift, We can open native iOS viewController from a flutter action event using the method channel,

  • In flutter, you need to add the following lines to trigger the native iOS method. Add these lines in your action method
var platform = const MethodChannel('com.nativeActivity/iosChannel');
final String result = await platform.invokeMethod('StartNativeIOS');
  • In iOS, add the following lines in the AppDelegate application method
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

let methodChannel = FlutterMethodChannel(name: "com.nativeActivity/iosChannel", binaryMessenger:controller.binaryMessenger)

methodChannel.setMethodCallHandler({
  (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        
  if call.method == "StartNativeIOS" {
    let mainVC = MainViewController() // Your viewController
    let navigationController = UINavigationController(rootViewController: mainVC)
    self.window.rootViewController = navigationController
    self.window.makeKeyAndVisible()
  } else {
    result(FlutterMethodNotImplemented)
    return
  }
})

It will work! cheers

Note: But there is no going back to the flutter view.

So here is the proper way of doing it. Flutter Navigation Using Coordinator Pattern

In Swift, We can open native iOS viewController from a flutter action event using the method channel, In flutter, you need to add the following lines to trigger the native iOS method. Add these lines in your action method

var platform = const MethodChannel('navigation');
final String result = await platform.invokeMethod('IOS');

In iOS, add the following lines in the AppDelegate application method.

let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "navigation", binaryMessenger:controller.binaryMessenger)
navigationController = UINavigationController(rootViewController: controller)
navigationController.setNavigationBarHidden(true, animated: false)
self.window!.rootViewController = navigationController
self.window!.makeKeyAndVisible()
methodChannel.setMethodCallHandler({
    (call: FlutterMethodCall, result: @escaping FlutterResult) -> 
    Void in
if call.method == "IOS" {
      let vc = "Your viewController"(nibName: "Your 
      viewController", bundle: nil)
      navigationController.pushViewController(vc, animated: 
     false)
} else {
         result(FlutterMethodNotImplemented)
         return
}
})

Native view controller back button action.

self.navigationController?.popViewController(animated: true)

Thank you.

Writing custom platform-specific code

Widget with a button. Tap on button should push rely message to push a view controller to the iOS app.

class BodyWidget extends StatelessWidget {

  static const platform = const MethodChannel('samples.flutter.dev/navigation_channel');

  Future<void> _pushOnNative() async {
    try {
      Map parameters = Map();
      parameters["title"] = "Contact Details";
      parameters["name"] = "Bang Operator";
      parameters["so_profile_exists"] = true;
      await platform.invokeMethod('push',[parameters]);
    } on PlatformException catch (e) {
      print("Failed to communicate level: '${e.message}'.");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Center(
        child: FlatButton(
          textColor: Colors.white,
          color: Colors.red,
          onPressed: _pushOnNative,
          child: Text('Push Me'),
        )
    );
  }
}

On iOS app side, We need to observe the channel samples.flutter.dev/navigation_channel for method 'push'. We can also pass arguments from flutter side, in this example we pass a Map(Dart) that is received as NSDictionary(Obj-C)

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];

  FlutterViewController *controller = (FlutterViewController*)self.window.rootViewController;
  UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller];

  self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  self.window.rootViewController = navigationController;
  [navigationController setNavigationBarHidden:YES animated:YES];
  [self.window makeKeyAndVisible];

  [self setupNavigationChannelCallbacksOnMessenger:controller
                           forNavigationController:navigationController];


  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)setupNavigationChannelCallbacksOnMessenger:(id<FlutterBinaryMessenger>)messenger
                           forNavigationController:(UINavigationController *)navigationController {
  FlutterMethodChannel* pushChannel = [FlutterMethodChannel
                                       methodChannelWithName:@"samples.flutter.dev/navigation_channel"
                                       binaryMessenger:messenger];

  [pushChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    if ([call.method isEqualToString:@"push"] && [self getDictionaryFromArguments:call.arguments]) {
      NSDictionary *parameters = [self getDictionaryFromArguments:call.arguments];
      [self pushContactScreenWithInfo:parameters onNaivgationController:navigationController];
      result(@(true));
    } else {
      result(FlutterMethodNotImplemented);
    }
  }];
}

- (NSDictionary *)getDictionaryFromArguments:(id)arguments {
  if (![arguments isKindOfClass:[NSArray class]]) {
    return nil;
  }
  NSArray *argumentsArray = (NSArray *)arguments;
  if (argumentsArray.firstObject == nil || ![argumentsArray.firstObject isKindOfClass:[NSDictionary class]]) {
    return nil;
  }
  return (NSDictionary *)argumentsArray.firstObject;
}

- (void)pushContactScreenWithInfo:(NSDictionary *)info
          onNaivgationController:(UINavigationController *)navigationContorller {
  UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  ContactViewController *controller = [storyboard instantiateViewControllerWithIdentifier:@"ContactViewController"];
  [navigationContorller pushViewController:controller animated:YES];
  [controller view];
  [controller configureWithInfo:info];
}
Related