MethodChannel: iOS Implementation in Swift
MethodChannel: iOS Implementation in Swift
Platform channels are Flutter's mechanism for calling native platform APIs that are not yet exposed through Flutter's own plugins or packages. On iOS, you implement the native side of a MethodChannel in Swift — either inside AppDelegate for simple use-cases, or inside a standalone FlutterPlugin class for reusable, well-structured integration. This lesson covers both approaches in detail.
How MethodChannel Works on iOS
When Dart invokes a method on a MethodChannel, Flutter serialises the method name and arguments using the standard message codec and delivers them to the iOS host. The iOS side receives an FlutterMethodCall object containing the method name and an arguments property. You must call the provided FlutterResult callback exactly once — passing back either a result value, nil, or a FlutterError.
FlutterResult callback is a Swift closure with the signature (Any?) -> Void. You must call it exactly once per method call. Calling it zero times leaves Dart awaiting forever; calling it more than once throws an assertion error in debug builds.Approach 1 — Registering the Channel in AppDelegate
For small projects or one-off integrations, you can register your FlutterMethodChannel directly inside AppDelegate.swift. The key steps are:
- Cast the root view controller to
FlutterViewController - Instantiate a
FlutterMethodChannelwith a name that matches the Dart side exactly - Set a method-call handler closure that switches on
call.method - For unknown methods, call
result(FlutterMethodNotImplemented)
AppDelegate.swift — Registering a MethodChannel
// ios/Runner/AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 1. Obtain the root FlutterViewController
let controller = window?.rootViewController as! FlutterViewController
// 2. Create the channel — name must match Dart exactly
let batteryChannel = FlutterMethodChannel(
name: "com.example.myapp/battery",
binaryMessenger: controller.binaryMessenger
)
// 3. Register the method-call handler
batteryChannel.setMethodCallHandler { [weak self] call, result in
guard let self = self else { return }
switch call.method {
case "getBatteryLevel":
let level = self.getBatteryLevel()
if level >= 0 {
result(level) // success: send Int back to Dart
} else {
result(FlutterError(
code: "UNAVAILABLE",
message: "Battery level not available",
details: nil
))
}
default:
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getBatteryLevel() -> Int {
UIDevice.current.isBatteryMonitoringEnabled = true
let level = UIDevice.current.batteryLevel
return level < 0 ? -1 : Int(level * 100)
}
}
Approach 2 — Implementing a FlutterPlugin
For reusable or more complex native integrations, the recommended pattern is to create a dedicated class that conforms to FlutterPlugin. This keeps AppDelegate lean and makes the plugin independently testable and distributable.
BatteryPlugin.swift — Standalone FlutterPlugin
// ios/Runner/BatteryPlugin.swift
import Flutter
import UIKit
public class BatteryPlugin: NSObject, FlutterPlugin {
// 1. Register is called once at app startup
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.example.myapp/battery",
binaryMessenger: registrar.messenger()
)
let instance = BatteryPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
// 2. Handle every inbound method call
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getBatteryLevel":
let level = fetchBatteryLevel()
if let level = level {
result(level)
} else {
result(FlutterError(
code: "UNAVAILABLE",
message: "Could not read battery level",
details: nil
))
}
case "isCharging":
UIDevice.current.isBatteryMonitoringEnabled = true
let charging = UIDevice.current.batteryState == .charging
|| UIDevice.current.batteryState == .full
result(charging) // sends Bool to Dart
default:
result(FlutterMethodNotImplemented)
}
}
private func fetchBatteryLevel() -> Int? {
UIDevice.current.isBatteryMonitoringEnabled = true
let raw = UIDevice.current.batteryLevel
guard raw >= 0 else { return nil }
return Int(raw * 100)
}
}
// In AppDelegate.swift, register the plugin in GeneratedPluginRegistrant
// or call: BatteryPlugin.register(with: registrar)
Sending Results and Errors Back to Dart
The FlutterResult closure accepts three categories of value:
- Success value — pass any type supported by the standard codec:
Int,Double,Bool,String,Data,[Any],[String: Any], ornil. - FlutterError — pass a
FlutterError(code:message:details:)instance; Dart receives aPlatformException. - FlutterMethodNotImplemented — a special sentinel that signals the method is not handled on the native side; Dart throws
MissingPluginException.
@escaping when you need to call it asynchronously (for example, after a network request or permission dialog). Mark it @escaping FlutterResult in the method signature and store it until you have a value to return.Parsing Incoming Arguments
Arguments passed from Dart are available as call.arguments typed as Any?. Cast them safely before use:
Safely Casting Arguments from Dart
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "setVolume":
// Dart sends: {'level': 0.75}
guard let args = call.arguments as? [String: Any],
let level = args["level"] as? Double else {
result(FlutterError(
code: "INVALID_ARGS",
message: "Expected {level: Double}",
details: nil
))
return
}
AudioSession.setOutputVolume(Float(level))
result(nil) // success with no return value
case "getDeviceModel":
// No arguments expected
let model = UIDevice.current.model // e.g. "iPhone"
result(model)
default:
result(FlutterMethodNotImplemented)
}
}
DispatchQueue.main.async { ... }. The FlutterResult callback itself is safe to call from any thread.Registering the Plugin in AppDelegate
When you use the standalone FlutterPlugin approach, you must register it during app startup. The canonical place is AppDelegate.swift, alongside or instead of the auto-generated plugin registrant:
- If using
GeneratedPluginRegistrant, callBatteryPlugin.register(with: registrar)insideapplication(_:didFinishLaunchingWithOptions:)afterGeneratedPluginRegistrant.register(with: self). - Alternatively, implement
registerPluginsin the Flutter engine's plugin registry.
Dart Side Recap
For completeness, here is the matching Dart code that invokes the channel registered above:
Dart — Calling the iOS MethodChannel
import 'package:flutter/services.dart';
class BatteryService {
static const _channel = MethodChannel('com.example.myapp/battery');
Future<int> getBatteryLevel() async {
try {
final int level = await _channel.invokeMethod('getBatteryLevel');
return level;
} on PlatformException catch (e) {
throw Exception('Failed to get battery level: ${e.message}');
}
}
Future<bool> isCharging() async {
final bool charging = await _channel.invokeMethod('isCharging');
return charging;
}
}
FlutterMethodChannel instance (created with a matching name and the engine's binaryMessenger) and a method-call handler that switches on call.method, casts call.arguments safely, and calls the FlutterResult callback exactly once per invocation. Use AppDelegate for simple cases and a dedicated FlutterPlugin class for reusable, production-grade integrations.