قنوات المنصة والتكامل الأصلي

MethodChannel: التنفيذ على iOS بـ Swift

16 دقيقة الدرس 4 من 11

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)
    }
}
تحذير: لا تصل أبداً إلى واجهات برمجة UIKit من خيط في الخلفية. إذا تم تنفيذ معالج استدعاء الطريقة خارج الخيط الرئيسي (نادر ولكن ممكن)، فغلّف أي استدعاءات UIKit داخل 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;
  }
}
النقطة الرئيسية: لتنفيذ MethodChannel على iOS تحتاج إلى شيئين — نسخة FlutterMethodChannel (مُنشأة باسم مطابق وبـ binaryMessenger المحرك) ومعالج استدعاء الطريقة الذي يُبدّل على call.method ويحوّل call.arguments بأمان ويستدعي دالة الرد FlutterResult مرةً واحدةً بالضبط لكل استدعاء. استخدم AppDelegate للحالات البسيطة وفئة FlutterPlugin المخصصة للتكاملات القابلة لإعادة الاستخدام والجاهزة للإنتاج.