MethodChannel: التنفيذ على iOS بـ Swift
MethodChannel: التنفيذ على iOS بـ Swift
قنوات المنصة (Platform Channels) هي آلية Flutter لاستدعاء واجهات برمجة المنصة الأصلية التي لم تُكشف بعد عبر مكونات Flutter الإضافية أو الحزم الخاصة بها. على iOS، تُنفَّذ الجانب الأصلي من MethodChannel بلغة Swift — إما داخل AppDelegate للحالات البسيطة، أو داخل فئة FlutterPlugin مستقلة للتكامل القابل لإعادة الاستخدام والمُنظَّم جيداً. يتناول هذا الدرس كلا النهجين بالتفصيل.
كيف يعمل MethodChannel على iOS
عندما تستدعي Dart طريقةً على MethodChannel، تقوم Flutter بتسلسل اسم الطريقة والوسيطات باستخدام برنامج ترميز الرسائل القياسي وتُسلّمها إلى المضيف على iOS. يتلقّى الجانب الأصلي كائن FlutterMethodCall يحتوي على اسم الطريقة في خاصية method والوسيطات في خاصية arguments. يجب استدعاء دالة الرد FlutterResult المُقدَّمة مرةً واحدةً بالضبط — بتمرير قيمة النتيجة أو nil أو كائن FlutterError.
FlutterResult هي إغلاق Swift بالتوقيع (Any?) -> Void. يجب استدعاؤها مرةً واحدةً بالضبط لكل استدعاء طريقة. عدم استدعائها يُبقي Dart في حالة انتظار أبدي؛ واستدعاؤها أكثر من مرة يُطلق خطأ تأكيد في وضع تصحيح الأخطاء.النهج الأول — تسجيل القناة في AppDelegate
للمشاريع الصغيرة أو التكاملات الفردية، يمكن تسجيل FlutterMethodChannel مباشرةً داخل AppDelegate.swift. الخطوات الأساسية هي:
- تحويل وحدة التحكم في العرض الجذرية إلى
FlutterViewController - إنشاء
FlutterMethodChannelباسم يطابق الجانب الـ Dart تماماً - تعيين إغلاق معالج استدعاء الطريقة الذي يُبدّل على
call.method - للطرق غير المعروفة، استدعاء
result(FlutterMethodNotImplemented)
AppDelegate.swift — تسجيل 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. الحصول على FlutterViewController الجذري
let controller = window?.rootViewController as! FlutterViewController
// 2. إنشاء القناة — يجب أن يطابق الاسم جانب Dart تماماً
let batteryChannel = FlutterMethodChannel(
name: "com.example.myapp/battery",
binaryMessenger: controller.binaryMessenger
)
// 3. تسجيل معالج استدعاء الطريقة
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) // نجاح: إرسال Int إلى 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)
}
}
النهج الثاني — تنفيذ FlutterPlugin
للتكاملات الأصلية القابلة لإعادة الاستخدام أو الأكثر تعقيداً، النمط الموصى به هو إنشاء فئة مخصصة تُطابق بروتوكول FlutterPlugin. هذا يُبقي AppDelegate نظيفاً ويجعل المكوّن الإضافي قابلاً للاختبار المستقل والتوزيع.
BatteryPlugin.swift — FlutterPlugin مستقل
// ios/Runner/BatteryPlugin.swift
import Flutter
import UIKit
public class BatteryPlugin: NSObject, FlutterPlugin {
// 1. يُستدعى register مرةً واحدة عند بدء تشغيل التطبيق
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. معالجة كل استدعاء طريقة وارد
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) // إرسال Bool إلى 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)
}
}
// في AppDelegate.swift، سجّل المكوّن الإضافي في GeneratedPluginRegistrant
// أو استدعِ: BatteryPlugin.register(with: registrar)
إرسال النتائج والأخطاء إلى Dart
يقبل إغلاق FlutterResult ثلاث فئات من القيم:
- قيمة النجاح — مرّر أي نوع مدعوم بالترميز القياسي:
IntأوDoubleأوBoolأوStringأوDataأو[Any]أو[String: Any]أوnil. - FlutterError — مرّر نسخة
FlutterError(code:message:details:)؛ تستقبل Dart استثناءPlatformException. - FlutterMethodNotImplemented — قيمة خاصة تُشير إلى أن الطريقة غير مُعالَجة على الجانب الأصلي؛ تُطلق Dart استثناء
MissingPluginException.
@escaping عند الحاجة إلى استدعائه بشكل غير متزامن (مثلاً بعد طلب شبكي أو مربع حوار الأذونات). ضع علامة @escaping FlutterResult في توقيع الطريقة وخزّنه حتى تحصل على قيمة للإرجاع.تحليل الوسيطات الواردة
الوسيطات المُمرَّرة من Dart متاحة عبر call.arguments من النوع Any?. حوّلها بأمان قبل الاستخدام:
التحويل الآمن للوسيطات القادمة من Dart
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "setVolume":
// Dart ترسل: {'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) // نجاح بدون قيمة إرجاع
case "getDeviceModel":
// لا وسيطات متوقعة
let model = UIDevice.current.model // مثال: "iPhone"
result(model)
default:
result(FlutterMethodNotImplemented)
}
}
DispatchQueue.main.async { ... }. دالة الرد FlutterResult نفسها آمنة للاستدعاء من أي خيط.تسجيل المكوّن الإضافي في AppDelegate
عند استخدام نهج FlutterPlugin المستقل، يجب تسجيله أثناء بدء تشغيل التطبيق. المكان الأساسي هو AppDelegate.swift، إلى جانب سجّل المكونات المُنشأ تلقائياً أو بدلاً منه:
- عند استخدام
GeneratedPluginRegistrant، استدعِBatteryPlugin.register(with: registrar)داخلapplication(_:didFinishLaunchingWithOptions:)بعدGeneratedPluginRegistrant.register(with: self). - بدلاً من ذلك، نفّذ
registerPluginsفي سجل مكونات Flutter الإضافية للمحرك.
مراجعة جانب Dart
للاكتمال، إليك كود Dart المطابق الذي يستدعي القناة المُسجَّلة أعلاه:
Dart — استدعاء MethodChannel على iOS
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 (مُنشأة باسم مطابق وبـ binaryMessenger المحرك) ومعالج استدعاء الطريقة الذي يُبدّل على call.method ويحوّل call.arguments بأمان ويستدعي دالة الرد FlutterResult مرةً واحدةً بالضبط لكل استدعاء. استخدم AppDelegate للحالات البسيطة وفئة FlutterPlugin المخصصة للتكاملات القابلة لإعادة الاستخدام والجاهزة للإنتاج.