How to call arguments from Flutter in Swift native code? - ios

I am trying to use PlatformViews in Flutter to show Swift code natively in my Flutter app, however my app is crashing with my current code.
This is my AppDelegate currently where I am invoking my method channel:
import Foundation
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate, TJPlacementDelegate {
var p = TJPlacement()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let channelName = "NativeView"
let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: channelName, binaryMessenger: rootViewController as! FlutterBinaryMessenger)
methodChannel.setMethodCallHandler {(call: FlutterMethodCall, result: FlutterResult) -> Void in
if (call.method == "setDebugEnabled") {
let isDebug = call.arguments as! Bool
Tapjoy.setDebugEnabled(isDebug)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
This is my Dart implementation for the native code:
import 'package:flutter/material.dart';
import 'tapjoy.dart';
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
callTapjoy();
super.initState();
}
Widget build(context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Test'),
),
body: UiKitView(viewType: 'NativeView'),
),
);
}
void callTapjoy() {
Tapjoy.setDebugEnabled(true);
}
}
//My code in tapjoy.dart
class Tapjoy {
static const MethodChannel _channel = const MethodChannel('NativeView');
static void setDebugEnabled(bool isDebug) {
_channel.invokeMethod('setDebugEnabled', {"isDebug": isDebug});
}
}
My app crashes and shows me an error in the debug console:
Could not cast value of type '__NSDictionaryM' (0x7fff87a61d78) to 'NSNumber' (0x7fff87b1eb08).
2020-04-29 16:56:42.985269+0530 Runner[18484:224162] Could not cast value of type '__NSDictionaryM' (0x7fff87a61d78) to 'NSNumber' (0x7fff87b1eb08).

You are passing a Map from Dart to native: {"isDebug": isDebug}, so you need extract the parameter from the map/dictionary at the Swift end.
if let args = call.arguments as? Dictionary<String, Any>,
let isDebug = args["isDebug"] as? Bool {
// please check the "as" above - wasn't able to test
// handle the method
result(nil)
} else {
result(FlutterError.init(code: "errorSetDebug", message: "data or format error", details: nil))
}
Alternatively, just pass the boolean from the Dart end, without first putting it into a map.
_channel.invokeMethod('setDebugEnabled', isDebug);

Related

Add delegate to custom iOS Flutter Plugin

I'm working on integrating a custom iOS plugin into my Flutter app, problem is that I'm not getting delegate callbacks from the custom SDK Protocol.
I have to connect a bluetooth device to my app and I from the delegate calls I should receive the device's ID and pair it.
From the Flutter side, I can call the native functions from the customSdk: sdkInstance.scan() and there are even some internal (inside the sdk) prints with the scan results but my delegate calls are not in place.
I think I'm not correctly adding the delegate to the SDK, I can get this to work in a swift native app but not as a Flutter Plugin.
So here's more or less the code:
iOS Code
AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
SwiftIosPlugin.swift
import Flutter
import UIKit
import CustomSDK
public class SwiftIosPlugin: NSObject, FlutterPlugin {
let sdkInstance = CustomSDK.shared // This returns an instance of the SDK
let channel: FlutterMethodChannel
public static func register(with registrar: FlutterPluginRegistrar)
let channel = FlutterMethodChannel(name: "ios_plugin_channel", binaryMessenger: registrar.messenger())
let instance = SwiftIosPlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
}
init (_ channel: FlutterMethodChannel) {
self.channel = channel
super.init()
// In Swift, this is done in viewDidLoad()
// Is this the correct place to do this?
sdkInstance.addDelegate(self)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
switch call.method {
case "startScan":
do {
// This is being called and results printed
try sdkInstance.scan()
} catch {
result(FlutterError(code: "400", message: "\(error)", details: nil))
}
case "connect":
sdkInstance.connect(call, result)
default:
result(FlutterMethodNotImplemented)
}
}
}
// These should be called but are not
extension SwiftIosPlugin: CustomSDKDelegate {
// Isn't called when scan() is executeed!
public func onScanDevice(didScan id:String) {
// do logic
}
public func onPairedDevice(didPair id:String) {
// do logic
}
}
Update:
Silly thing that I hope nobody else has this trouble...
Two things to consider:
The problem was some of the delegate's functions public func onScanDevice(didScan id:String) was missing a parameter (even though there weren't any errors pointed out by Xcode).
sdkInstance.addDelegate(self) was called too early in the class "lifecycle".
Be mindful of these things and you won't have any trouble!

flutter : The screen freezes when I try to output the iOS built-in dictionary with UIReferenceLibraryViewController

I am making an English word learning app with flutter.
I am using the built-in dictionary of iOS.
It is a basic application that the built-in dictionary is displayed when the user presses the "answer confirmation button". I can basically do what I want to do, but I found a bug yesterday.
When I pressed the answer confirmation button for the word "buffalo", the screen froze. The screen itself is displayed instead of being killed, but no further operations are accepted.
It's the first time I've tested over 500 words. The other words are working fine.
The method channel is used to call the built-in dictionary.
//AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var ref: UIReferenceLibraryViewController = UIReferenceLibraryViewController(term:"")
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "hello_ios", binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: FlutterResult) -> Void in
if call.method == "searchDictionary" {
print(call.arguments)
self.searchDictionary(result:result,controller:controller,queryWord:call.arguments as! String)
//result("Hello from iOddddddddddddS")
} else {
result(FlutterMethodNotImplemented)
}
})
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func searchDictionary(result: FlutterResult, controller: FlutterViewController, queryWord: String){
print("416\(UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm:queryWord))")
//let ref: UIReferenceLibraryViewController = UIReferenceLibraryViewController(term: queryWord)
ref = UIReferenceLibraryViewController(term:queryWord)
if(UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm:queryWord)){
controller.present(ref, animated: true, completion: nil)
}
}
}
I wrote the above code thinking that it should be conditional branching by
"UIReferenceLibraryViewController.dictionaryHasDefinition",
but it seems that it is not a solution because true is returned even at the time of "buffalo".
Since it works normally with other words, it seems that the caller is not the cause, but the following is the caller.
onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
await HomePage._channel.invokeMethod('searchDictionary',
editingTargetTextCtrl.text != '' ? editingTargetTextCtrl.text : data == null ? '' : data.text,);
},
There are almost no error messages, so I can't get any clues. Is there anything?

Could not cast value of type 'FlutterViewController' to 'UINavigationController'

I'm trying to run an implementation of mercado_pago_mobile_checkout in my Flutter project. When I run in Android Emulator, the function StartCheckout works correctly, but in iOS version, when the function StartCheckout is called, this error occurs:
2021-07-22 11:39:14.504430-0300 Runner[31296:218261] Could not cast value of type 'FlutterViewController' (0x10b5bab60) to 'UINavigationController' (0x128b638f0).
The code that is seems to be broken is the AppDelegate.swift:
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var navigationController: UINavigationController?;
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let flutterViewController: FlutterViewController = window?.rootViewController as! FlutterViewController
GeneratedPluginRegistrant.register(with: self)
self.navigationController = UINavigationController(rootViewController: flutterViewController);
self.navigationController?.setNavigationBarHidden(true, animated: false);
self.window = UIWindow(frame: UIScreen.main.bounds);
self.window.rootViewController = self.navigationController;
self.window.makeKeyAndVisible();
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
The code of the function StartCheckout is:
PaymentResult result = await MercadoPagoMobileCheckout.startCheckout(
publicKey,
_idCompra,
// ignore: missing_return
).then((result) async {
if (result.status != null) {
bd
.collection("bagagens")
.document(_bagagem.id)
.setData(_bagagem.toMap())
.then((_) {
bd
.collection("minhas_bagagens")
.document(_bagagem.idUsuario)
.collection("bagagens")
.document(_bagagem.id)
.setData(_bagagem.toMap())
.then((_) {
// cria a bagagem comprada
bd
.collection("minhas_bagagens_compradas")
.document(idUsuarioLogado)
.collection("bagagens_compradas")
.document(_bagagemComprada.id)
.setData(_bagagemComprada.toMap())
.then((_) {
//salvar bagagem pública
bd
.collection("bagagens_compradas")
.document(_bagagemComprada.id)
.setData(_bagagemComprada.toMap())
.then((_) async {
if (_compraComSaldo) {
Map<String, dynamic> dadosAtualizados = {"saldo": "0.00"};
bd
.collection("usuarios")
.document(_idUsuarioLogado)
.updateData(dadosAtualizados);
}
Navigator.pop(_dialogContext);
Navigator.push(context,
MaterialPageRoute(builder: (context) => BarraPrincipal(0)));
/// Notificação para o usuário vendedor
await notificarUsuario([
_idOneSignal
], "Um usuário solicitou a compra de sua bagagem no voo ${_bagagem.identificadorVoo}.",
"Bagagem Solicitada");
/// End Notificação
});
});
});
});
} else {
Navigator.pop(_dialogContext);
}
});
Somebody knows how to solve this error? Thanks for the attention.
Change
let flutterViewController: FlutterViewController = window?.rootViewController as! FlutterViewController
to
let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)

How do I use a Flutter MethodChannel to invoke a method in dart code from the native swift code?

I have looked at many similar questions on this topic, but none of the solutions have worked for me.. I am developing an App in Flutter, but want to call a specific method in my main.dart file from AppDelegate.swift in the native iOS project.
To remove all other variables I have extracted the issue into a fresh dart project. I am trying to call setChannelText() from AppDelegate.swift using methodChannel.invokeMethod(), but with no success.
Does anybody know where I am going wrong? I know I'm not acting upon the "name" parameter in methodChannel.invokeMethod(), but that's because I only want the call to invoke the method at all...
Here is my main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
MethodChannel channel =
new MethodChannel("com.example.channeltest/changetext");
String centerText;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.purple,
body: Center(
child: Text(
centerText,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 30.0,
),
),
),
),
);
}
#override
void initState() {
super.initState();
this.channel.setMethodCallHandler((call) async => await setChannelText());
this.centerText = "Hello World!";
}
Future setChannelText() async {
Future.delayed(Duration(milliseconds: 200));
setState(() => this.centerText = "Another Text.");
}
}
And here is my AppDelegate.swift file:
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var methodChannel: FlutterMethodChannel!
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
methodChannel = FlutterMethodChannel(name: "com.example.channeltest/changetext", binaryMessenger: rootViewController as! FlutterBinaryMessenger)
//This call would obviously be somewhere else in a real world example, but I'm just
//testing if I can invoke the method in my dart code at all..
methodChannel.invokeMethod("some_method_name", arguments: nil)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
In the end, I am trying to get the text to change right after launch, but it doesn't.
Screenshot of app running on iOS simulator
Thanks in advance for any help!!
Issue
The issue is your platform side (iOS in this case) is calling a method on the Flutter side before Flutter is ready. There is no way to check from the platform side, so your Flutter app must tell your platform side. You'll have the same problem on Android.
Solution
To overcome this, you have to tell the Platform side that the app is ready (by sending a platform method) and save it in a boolean, or instantiate a class, and calling a method. Then the platform side can start sending messages.
You should really read the logs, it should warn you something along the lines of: "There is nothing listening to this, or the Flutter Engine is not attached".
import 'dart:async';
import 'package:flutter/src/services/platform_channel.dart';
class StringService {
final methodChannel =
const MethodChannel("com.example.app_name.method_channel.strings");
final StreamController<String> _stringStreamController =
StreamController<String>();
Stream<String> get stringStream => _stringStreamController.stream;
StringService() {
// Set method call handler before telling platform side we are ready to receive.
methodChannel.setMethodCallHandler((call) async {
print('Just received ${call.method} from platform');
if (call.method == "new_string") {
_stringStreamController.add(call.arguments as String);
} else {
print("Method not implemented: ${call.method}");
}
});
// Tell platform side we are ready!
methodChannel.invokeMethod("isReady");
}
}
You can see a working project at reverse_platform_methods, especially AppDelegate.swift. I didn't implement it for Android, but you can do it in a similar way in MainActivity.kt.
Question
Most apps don't want code to call from the platform side first. What is your use case? I can possibly provide better advice depending on your answer. I implemented this to handle push notifications being delivered to the device, so the "event" is definitely triggered from the platform side.
Also, you should show errors and warnings if you face them, e.g. No implementation found for method $method on channel $name'.
Well, the problem is all about the initialization process. You try to call your method from swift code BEFORE the dart/flutter part is ready to handle it.
You have to do the next steps to achieve the result:
Important. Use applicationDidBecomeActive method in your AppDelegate for ios
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var methodChannel: FlutterMethodChannel? = nil
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
print("Setup methodChannel from Swift")
let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
methodChannel = FlutterMethodChannel(name: "com.example.channeltest/changetext", binaryMessenger: rootViewController as! FlutterBinaryMessenger)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
//THIS METHOD
override func applicationDidBecomeActive(_ application: UIApplication) {
methodChannel?.invokeMethod("some_method_name", arguments: "ios string")
}
}
For Android onStart() method:
class MainActivity : FlutterActivity() {
var channel: MethodChannel? = null
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.channeltest/changetext"
)
}
override fun onStart() {
super.onStart()
channel?.invokeMethod("some_method_name", "android str")
}
}
Create your own class with MethodChannel(like from prev. answer)
class TestChannel {
static MethodChannel channel =
const MethodChannel("com.example.channeltest/changetext");
final StreamController<String> _controller =
StreamController<String>();
Stream<String> get stringStream => _controller.stream;
TestChannel() {
channel.setMethodCallHandler((call) async {
if (call.method == "some_method_name") {
_controller.add(call.arguments as String);
} else {
print("Method not implemented: ${call.method}");
}
});
}
}
Important. Create it global instance
final _changeTextChannel = TestChannel(); //<--- like this
void main() {
runApp(MyApp());
}
Handle it in UI
class TestPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<String>(
stream: _changeTextChannel.stringStream,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasError) {
return Text("Error");
}
if (!snapshot.hasData) {
return Text("Loading");
}
return Text(snapshot.data ?? "NO_DATA");
},
)),
);
}
}
Flutter side code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
static const platform = MethodChannel('samples.flutter.dev/battery');
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
}
Swift code here:
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
// Note: this method is invoked on the UI thread.
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self?.receiveBatteryLevel(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Or refer to this link:
Platform Channels

How to call (Invoke) dart method from swift native code (AppDelegate)??The "invokeMethod" is working on android but not in ios

I'm trying to call function("onRazorPayPaymentFail" method) in dart
file (main.dart) from AppDelegate class of ios using invokeMethod of
flutter channel.But it is not working.The method onRazorPayPaymentFail
is firing on Android.But In iOS it is not working.
//main.dart from lib folder
//main.dart from lib folder
class _MyHomePageState extends State<MyHomePage> {
static const razorpay_platform =
const MethodChannel('com.algorin.pf.razorpay');
#override
void initState() {
super.initState();
razorpay_platform.setMethodCallHandler(myUtilsHandler);
}
Future<dynamic> myUtilsHandler(MethodCall methodCall) async {
switch (methodCall.method) {
case 'onRazorPayPaymentFail':
print(json.decode(methodCall.arguments));
break;
case 'onRazorPayPaymentSuccess':
processSuccessResponse(json.decode(methodCall.arguments));
break;
default:
}
}
//..................................
}}
And AppDelegate.Swift file
//AppDelegate.Swift file
import UIKit
import Flutter
import Razorpay
#UIApplicationMain
#objc class AppDelegate : FlutterAppDelegate,
RazorpayPaymentCompletionProtocol {
//..............
var controller : FlutterViewController!;
var RAZORPAY_IO_CHANNEL : FlutterMethodChannel!;
override func application (
_ application : UIApplication,
didFinishLaunchingWithOptions launchOptions : [UIApplicationLaunchOptionsKey: Any]?)->
Bool {
controller = window?.rootViewController as? FlutterViewController;
RAZORPAY_IO_CHANNEL = FlutterMethodChannel (
name : "com.algorin.pf.razorpay",
binaryMessenger : controller
)
GeneratedPluginRegistrant.register (with: self);
return super.application (
application,
didFinishLaunchingWithOptions : launchOptions
);
}
public func onPaymentError (_ code: Int32, description str: String)
{
print (
"<<<<<<<<<<< Payment Failed >>>>>>>>>>>>>>>>>>>>>>>>>>>"
);
RAZORPAY_IO_CHANNEL.invokeMethod ("onRazorPayPaymentFail", arguments: str);
}
//............
}

Resources