How to call native iOS function from Flutter using the Pigeon package?

Issue

I am trying to integrate some native code in my project. For that, I am using the new Pigeon package: https://pub.dev/packages/pigeon

On Android it works just fine but on iOS, whenever I try to call the method written in swift (on button press), it throws a PlatformException(channel-error, Unable to establish connection on channel., null, null)

I’ve setup the flutter starting project and then I am calling the native methods when the floatingActionButton is pressed. When the native method is executed, it returns a String saying "Hello from Android" on the android side and "Hello from Xcode" on iOS.

main.dart file:

import 'package:flutter/material.dart';
import 'package:platforms/pigeon.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          Api api = Api();
          final searchReply = await api.search(SearchRequest());
          print(searchReply.result);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

MainActivity.java file:

package com.example.platforms;
import android.os.Bundle;
import io.flutter.embedding.android.FlutterActivity;

public class MainActivity extends FlutterActivity {
    private class Api implements Pigeon.Api {

        @Override
        public Pigeon.SearchReply search(Pigeon.SearchRequest arg) {
            Pigeon.SearchReply searchReply = new Pigeon.SearchReply();
            searchReply.setResult("Hello from Android!");
            return searchReply;
        }
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Pigeon.Api.setup(getFlutterEngine().getDartExecutor().getBinaryMessenger(), new Api());
    }
    
}

AppDelegate.swift file:

import UIKit
import Flutter




@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, Api {
    var engine: FlutterEngine = {
        let result = FlutterEngine.init()
        // This could be `run` earlier in the app to avoid the overhead of doing it the first time the
        // engine is needed.
        result.run()
        return result
      }()
    
    
    func search(_ input: SearchRequest, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> SearchReply? {
        
        let reply = SearchReply()
        
        reply.result = "Hello from Xcode!!!!"
        return reply
    }
    
    
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    
    GeneratedPluginRegistrant.register(with: self)
    ApiSetup(engine.binaryMessenger, self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Result of onPressed on Android:
works as expected

Result of onPressed on iOS:
throws error

What’s also worth noting is that when I call the code in onPressed in the didChangedDependencies function, the code in swift does run, and the result does show up, but the same exception is then thrown again. Which means that the channel does get created, I am just using it wrong.

When this is added to main.dart

@override
  void didChangeDependencies() async {
    Api api = Api();
    final searchReply = await api.search(SearchRequest());
    print(searchReply.result);
    super.didChangeDependencies();
  }

prints result before throwing error

It prints the result from swift and then throws the same error.

Solution

I think the BinaryMessenger is getting set incorrectly.

I tried this and it worked:
In the didFinishLaunchingWithOptions method get the rootViewController and cast it as FlutterBinaryMessenger:

let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
// get binaryMessenger
let binaryMessenger = rootViewController as! FlutterBinaryMessenger
    
// set binaryMessenger
ApiSetup(binaryMessenger, self)

The complete method becomes like this:

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self);
    
    let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
    
    // get binaryMessenger
    let binaryMessenger = rootViewController as! FlutterBinaryMessenger
    
    // set binaryMessenger
    ApiSetup(binaryMessenger, self)

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

Answered By – Abdullah Umer

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *