---
title: "البدء مع Twilio Voice على iOS: استقبال مكالمات VoIP"
description: "استقبال مكالمات VoIP باستخدام CallKit وTwilio Voice على iOS"
canonical: https://oozou.com/ar/blog/getting-started-with-twilio-voice-for-ios-accepting-voip-calls-145
locale: ar
date: 2020-12-19
author: "OOZOU Team"
---

# البدء مع Twilio Voice على iOS: استقبال مكالمات VoIP

> استقبال مكالمات VoIP باستخدام CallKit وTwilio Voice على iOS

Published: 2020-12-19 · Author: OOZOU Team (Engineering) · 13 min read

**ما هو VoIP؟**
VoIP اختصار لعبارة "Voice over Internet Protocol"، أي نقل الصوت عبر بروتوكول الإنترنت. وباختصار، يعني استخدام الإنترنت لإجراء المكالمات الهاتفية بدلًا من تقنيات الهاتف التقليدية. ويتفوّق على الهواتف التناظرية التقليدية بمزايا عديدة، منها انخفاض التكلفة ومرونة أكبر في الإعدادات المخصصة مثل تحويل المكالمات والرد الآلي أثناء الانتظار وتسجيل المكالمات.

# ما هو Apple CallKit؟

Apple CallKit إطار عمل قوي قدّمته Apple يتيح للمطوّرين دمج تطبيقات VoIP مع واجهة الاتصال الأصلية في iOS. ويوفّر تجربة سلسة، إذ تظهر مكالمات VoIP تمامًا مثل المكالمات الهاتفية العادية على iPhone. وهذا يساعد المستخدمين على إدارة مكالماتهم بكفاءة أكبر، ويضمن أن تبدو تطبيقات VoIP وتعمل بالشكل نفسه الذي تعمل به وظائف الهاتف القياسية.

# كيف تبنون تطبيق VoIP باستخدام iOS CallKit وSendbird Calls

يمرّ بناء تطبيق VoIP باستخدام iOS CallKit وSendbird Calls بعدة خطوات رئيسية. أولًا، تحتاجون إلى إعداد المشروع في Xcode ودمج أطر العمل اللازمة، ومنها CallKit وPushKit. ويمكن استخدام Sendbird Calls SDK للتعامل مع جوانب الاتصال، فهو يوفّر ميزات متينة للمكالمات الصوتية ومكالمات الفيديو. وستحتاجون كذلك إلى التسجيل في الإشعارات الفورية (push notifications) ومعالجة أحداث المكالمات الواردة باستخدام CallKit، لضمان عرض المكالمات للمستخدم حتى عندما يكون التطبيق في الخلفية.

# حالات استخدام iOS CallKit

يفيد iOS CallKit بشكل خاص في الحالات التي يكون فيها الدمج السلس للمكالمات أمرًا حاسمًا. فالشركات التي تملك تطبيقات لدعم العملاء مثلًا تستطيع استخدام CallKit لضمان التعامل مع مكالمات الدعم بكفاءة المكالمات الهاتفية العادية نفسها. كما يمكن لتطبيقات التواصل الاجتماعي الاستفادة من CallKit لتعزيز تفاعل المستخدمين عبر ميزات مدمجة للمكالمات الصوتية ومكالمات الفيديو.

# فهم فئات CallKit وتفاصيل الـ API

يتكوّن CallKit من عدة فئات (classes) وواجهات API رئيسية يحتاج المطوّرون إلى فهمها لدمجه بفعالية في تطبيقاتهم. فالفئة CXProvider مسؤولة عن الإبلاغ عن أحداث المكالمات وإدارتها، بينما تُستخدم CXCallController لإدارة إجراءات المكالمة التي يبدأها المستخدم. وفهم هذه المكوّنات ضروري لبناء تطبيق VoIP متين يستفيد من قدرات CallKit كاملة.

# المتطلبات المسبقة
\- يغطّي هذا الدليل جزء iOS من تطبيق VoIP مبني على Twilio. ولا بد أن يكون لديكم أيضًا مكوّن على الخادم وحساب Twilio مع مفاتيح API لخدمة Twilio Voice.

# البداية
نحتاج إلى عدة أمور لبناء جانب iOS من التطبيق:
1) إطار عمل CallKit: يتيح لتطبيقنا استخدام واجهة الهاتف الأصلية في iOS للتحكّم في المكالمات، وسنستقبل callbacks عند النقر على كل عنصر في الواجهة.
2) إطار عمل PushKit: يتيح لتطبيقنا استقبال الإشعارات الفورية للمكالمات الواردة. وهو مسؤول عن التسجيل في الإشعارات، ويمنحنا token فريدًا نرسله إلى Twilio. وسنحتاج أيضًا إلى إنشاء شهادة VoIP لهذا الغرض في حساب Apple Developer ورفعها إلى لوحة تحكم Twilio.
3) إطار عمل Twilio Voice (عبر cocoapod): يربط تطبيقنا بخدمة Twilio. وستحتاجون إلى إضافة _pod 'TwilioVoice'_ إلى ملف Podfile.

# فهم مسار المكالمة
من المهم أن تفهموا ما يحدث فعليًا عندما يتصل بكم شخص ما وتُحوَّل المكالمة إلى تطبيق iOS الخاص بكم.
\- أولًا، يجب أن يكون رقم الهاتف الذي يتصل به المستخدم الخارجي مسجّلًا لدى Twilio. وقد يكون رقمًا توفّره Twilio، أو رقمكم الخاص بعد تسجيله لديها.
\- عندما يتصل المستخدم الخارجي بالرقم، تجلب Twilio معلومات هذا الرقم وتنفّذ إجراءات وفق المحتوى الذي عرّفتموه على الخادم باستخدام TwiML.
\- يحدّد السلوك المعرَّف في TwiML عميلًا (client) بمعرّف فريد للاتصال به. ويمكن أن يكون هذا المعرّف أي نص، لكن بما أن المكالمات مرتبطة بعميل محدد، فعادةً ما يُولَّد على جانب iOS ثم يُرسل إلى الخادم ويُحفظ هناك.
\- يرتبط هذا المعرّف بـ push token فريد للجهاز (كما ذكرنا أعلاه)، وبناءً على هذه المعلومات تعرف Twilio أي جهاز iOS يجب الاتصال به.
\- تصل المكالمة إلى iOS وتمرّ أولًا بدالة الـ delegate في PKPushRegistry: "didReceiveIncomingPushWith payload: PKPushPayload..."
\- نمرّر الإشعار وحمولته (payload) إلى Twilio
\- تستدعي Twilio الـ delegate الخاص بها، وهناك نُبلغ CallKit بالمكالمة
\- يُعلم CallKit نظام iOS بالمكالمة الواردة، فتظهر على واجهة الهاتف
\- عندما تضغطون زر الهاتف الأخضر في واجهة الهاتف للرد، يُستدعى CXProviderDelegate لتنفيذ CXAnswerCallAction. وهنا نستخدم Twilio أخيرًا للرد على المكالمة فعليًا وإنشاء الاتصال الصوتي.

# الخطة
مع وضع ما سبق في الاعتبار، لنضع خطة لإعداد الأمور على عميل iOS:
\- إضافة Twilio Voice pod إلى ملف Podfile
\- بعد ذلك، التسجيل في إشعارات VoIP باستخدام PushKit
\- ثم إرسال device token إلى Twilio
\- وبشكل منفصل، نحتاج إلى جلب access token من Twilio يوفّره مكوّن الخادم في تطبيقكم. تنتهي صلاحية هذه الـ tokens كل 1 إلى 24 ساعة، لذا ستحتاجون إلى طريقة لتجديدها. لكن تجدر الإشارة إلى أنكم تستطيعون من الناحية العملية _استقبال_ المكالمات حتى بعد انتهاء صلاحية الـ access token. غير أنكم لن تتمكنوا من التحكّم في المكالمة من جانب العميل عبر Twilio ما دام الـ token منتهي الصلاحية، كإنهاء المكالمة مثلًا، لأن ذلك يتطلب استدعاء API داخليًا لدى Twilio يستخدم الـ access token للمصادقة.
\- سنحتاج إلى إعداد CallKit بإنشاء كائن CXProvider وكائن CXCallController.

# الإعداد
أولًا، انتقلوا إلى تبويب Signing and Capabilities في مشروع Xcode، وتأكدوا من تفعيل الإمكانات التالية لتطبيقكم ضمن Background Modes:
وتحتاجون أيضًا إلى إضافة إمكانية Push Notifications.
بعد ذلك، أنشئوا إدخالًا في ملف info.plist للمفتاح "NSMicrophoneUsageDescription" مع وصف يوضّح سبب حاجتكم إلى الوصول إلى الميكروفون.

```
<key>NSMicrophoneUsageDescription</key>
<string>For answering VoIP calls</string>
```

# التسجيل في إشعارات VoIP
لهذه الخطوة جزءان: جزء الكود وجزء الشهادة. لنبدأ بالكود، ونهيّئ كائن PKPushRegistry:

```
let voipRegistry = PKPushRegistry(queue: .main)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [.voIP]
```

هناك 3 دوال يجب تنفيذها للتوافق مع PKPushRegistryDelegate. وفي هذه المرحلة من دورة حياة التطبيق، يُفترض أنكم جلبتم الـ access token من الخادم بالفعل:

```
import TwilioVoice
```
```
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
    // Here call TwilioVoice.register(accessToken: accessToken, deviceToken: pushCredentials.token)
    // with the push credentials
}

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
    // Here call TwilioVoice.unregister(accessToken: accessToken, deviceToken: pushCredentials.token)
}

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { }
```

هذا الكود واضح في معظمه. فعند استقبال بيانات اعتماد الإشعارات، ترسلونها إلى Twilio مع الـ access token. أما الدالة الأخيرة didReceiveIncomingPushWith فهي التي سنستخدمها لمعالجة المكالمات الواردة، وسننفّذها لاحقًا.
والآن إلى الشهادة. هذا ليس شرحًا لماهية الشهادات أو طريقة إنشائها بالتفصيل، لكن باختصار، ستحتاجون إلى ما يلي:
1) إنشاء طلب توقيع شهادة (certificate signing request) على جهاز Mac
2) تسجيل الدخول إلى حساب Apple Developer على developer.apple.com
3) الانتقال إلى Certificates, Identifiers, and Profiles
4) اختيار "Certificates"
5) إنشاء شهادة جديدة من نوع VoIP Services باستخدام طلب توقيع الشهادة
6) تنزيل الشهادة على جهازكم
7) فتح الشهادة في Keychain وتصديرها كملف .p12.
8) استخراج الشهادة والمفتاح الخاص من ملف .p12 كما هو موضّح هنا: https://github.com/twilio/voice-quickstart-ios
8) رفع بيانات الشهادة إلى Twilio عبر Dashboard -> Settings -> Credentials -> Push Credentials. ودوّنوا قيمة SID الخاصة بالشهادة، لأن مكوّن الخادم في تطبيقكم سيحتاج إليها عند إعداد Twilio.

# CallKit
من المهم أن تدركوا أن CallKit إطار عمل على مستوى الواجهة. فهو يوفّر واجهة نظام iOS تتفاعل معها المكالمة، ونستقبل من خلالها callbacks حول الأحداث المتعلقة بالواجهة، مثل كتم المستخدم للمكالمة أو ضغطه زر "Hold" أو زر الهاتف الأحمر لإنهاء المكالمة. لكن CallKit نفسه ليس الإطار الذي يكتم المكالمة فعليًا، فذلك من مهمة كائن **Call** من Twilio، حيث يمكننا مثلًا ضبط call.isMuted = true. كانت مكالمات VoIP عبر تطبيقات iOS ممكنة قبل ظهور CallKit في 2016، وكل ما يضيفه CallKit هو تمكين تطبيق VoIP من التفاعل مع واجهة الهاتف في النظام.
في CallKit سنعمل أساسًا مع كائنين: أحدهما من نوع CXProvider والآخر من نوع CXCallController:

```
let callKitCallController: CXCallController = CXCallController()
let configuration: CXProviderConfiguration = CXProviderConfiguration(localizedName: "VoIP Tutorial")
let callKitProvider: CXProvider = CXProvider(configuration: configuration)
callKitProvider.setDelegate(self, queue: nil)
```

تمرير nil إلى المعامل queue يعني استقبال callbacks الخاصة بالـ delegate على الـ main queue افتراضيًا. والفئة CXProvider هي التي نستخدمها لإبلاغ iOS بالإجراءات الخارجية التي تصل إلى تطبيقنا، مثل المكالمات الواردة أو إنهاء المكالمة من الطرف الآخر. أما كائن CXCallController فيُستخدم لإبلاغ iOS بإجراءات المستخدم المحلية داخل التطبيق، مثل بدء مكالمة أو الرد عليها أو إنهائها بالضغط على زر مخصص في تطبيقنا.

وسنحتاج أيضًا إلى التوافق مع CXProviderDelegate:

```
func providerDidReset(_ provider: CXProvider) { }

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {

}

func provider(_ provider: CXProvider, perform action: CXEndCallAction) { 

}
```

وهناك دوال أخرى أيضًا، مثل تلك الخاصة بـ CXStartCallAction أو CXSetHeldCallAction. ويمكنكم مراجعة توثيق CXProviderDelegate للاطلاع على القائمة الكاملة: https://developer.apple.com/documentation/callkit/cxproviderdelegate

# تجميع الأجزاء معًا
لنعد إلى الدالة func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith... التي رأيناها سابقًا. يستدعي iOS هذه الدالة عندما نستقبل إشعارًا لمكالمة صوتية جديدة. وهنا نحتاج إلى توجيه تطبيقنا لاستخدام Twilio في معالجة هذه المكالمة:

```
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {    
    TwilioVoice.handleNotification(payload.dictionaryPayload, delegate: self, delegateQueue: nil)    
    completion()
}
```

لننشئ فئة **CallClient** لتتبّع تفاصيل المكالمة. سنحتاج إلى الاحتفاظ بمراجع لعدة بيانات مختلفة:

```
import TwilioVoice
import CallKit

class CallClient: NSObject {
    var activeCallInvites: [String: CallInvite] = [:] // CallInvite is a type from Twilio voice
    var activeCall: Call? // from Twilio Voice
    var activeCalls: [String: Call] = [:]
    var callKitProvider: CXProvider!
    var callKitCallController: CXCallController = CXCallController()
    let defaultAudioDevice = DefaultAudioDevice()
}
```

بعد ذلك نتوافق مع NotificationDelegate الخاص بـ TwilioVoice:

```
extension CallClient: NotificationDelegate {

    func callInviteReceived(callInvite: CallInvite) {
        guard let from = callInvite.from else { return }
        let callHandle = CXHandle(type: .generic, value: from)
        let callUpdate = CXCallUpdate()
        callUpdate.remoteHandle = callHandle
        callUpdate.hasVideo = false
        activeCallInvites[callInvite.uuid.uuidString] = callInvite
        // will pass incoming call to CallKit
        callKitProvider.reportNewIncomingCall(with: callInvite.uuid, update: callUpdate) { error in
            //...
        }
    }

    func cancelledCallInviteReceived(cancelledCallInvite: CancelledCallInvite, error: Error) { }
}
```

ثم ننفّذ CXCallProviderDelgate للرد على المكالمة:

```
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    defaultAudioDevice.block()

    answerCall(uuid: action.callUUID)
    action.fulfill()
}

func answerCall(uuid: UUID) {
    guard let callInvite = activeCallInvites[uuid.uuidString] else { return }

    let acceptOptions = AcceptOptions(callInvite: callInvite) { builder in
        builder.uuid = callInvite.uuid
    }

    let call = callInvite.accept(options: acceptOptions, delegate: self)
    activeCall = call
    activeCalls[call.uuid!.uuidString] = call

   activeCallInvites.removeValue(forKey: uuid.uuidString)
}
```

ببساطة، نستقبل دعوة المكالمة، ونضيفها إلى قاموس الدعوات الحالية، ثم نقبل المكالمة الخاصة بـ UUID معيّن. والاستدعاء "callKitProvider.reportNewIncomingCall.." هو ما يُبلغ iOS فعليًا بالمكالمة الواردة ويعرض واجهة الرد والرفض الأصلية. وعند الانتهاء نزيل الـ UUID من دعوات المكالمات ونعيّن هذه المكالمة قيمةً لـ activeCall.
تُستدعى الدالة func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) عندما يختار المستخدم زر الهاتف الأخضر للرد على المكالمة.
والـ activeCall كائن من نوع **Call** يحتوي على معلومات مثل رقمي "from" و "to"، والـ SID الخاص بالمكالمة، وما إذا كانت المكالمة مكتومة أو قيد الانتظار حاليًا.
ونحتاج إلى استدعاء .fulfill() على الإجراء لإبلاغ CallKit بنجاحه. وفي حال الفشل، يجب استدعاء action.fail() بدلًا من ذلك.

# إنهاء المكالمة
لإنهاء المكالمة من زر مخصص داخل تطبيقنا، سنستخدم كائن CXCallController:

```
func hangupButtonPressed(sender: UIButton) {
    guard let uuid = activeCall?.uuid else { return }
    let endCallAction = CXEndCallAction(call: uuid)
    let transaction = CXTransaction(action: endCallAction)
    callKitCallController.request(transaction) { error in
       //...
    }
}
<br>
```

سيؤدي ذلك إلى استدعاء _CXEndCallAction_ في CXProviderDelegate، وهناك نطلب من Twilio قطع الاتصال:

```
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
    if let call = activeCalls[action.callUUID.uuidString] {
        call.disconnect()
    }
    action.fulfill()
}
```

وإذا أردنا إنهاء المكالمة من واجهة الهاتف في النظام بالضغط على زر القطع الأحمر، فمن المهم ملاحظة أن ذلك وحده لا يُنهي المكالمة فعليًا. فكل ما يفعله هو استدعاء _CXEndCallAction_ كما في المثال أعلاه. وفي تلك الدالة يجب أن نطلب من Twilio قطع المكالمة، وأن نستدعي _action.fulFill()_ أيضًا، وعندها فقط تنتهي المكالمة تمامًا.

# إدارة الصوت
هناك بعض الأمور التي ستحتاجون إليها لإدارة الصوت في التطبيق. أولًا، يجب طلب إذن الوصول إلى الميكروفون باستخدام:

```
AVAudioSession.sharedInstance().requestRecordPermission { permissionGranted in
   // permissionGranted is bool
}
```

ويمكنكم معرفة حالة الإذن الحالية عبر AVAudioSession.sharedInstance().recordPermission. وسنحتاج أيضًا إلى إعداد جلسة الصوت الافتراضية قبل أن نتمكن من استقبال أي مكالمات:

```
TwilioVoice.audioDevice = audioDevice
```

وكما ذكرنا أعلاه، عندما يستدعي CXProviderDelegate الإجراء CXAnswerCallAction، يجب إعداد جلسة الصوت:

```
defaultAudioDevice.block()
```

# مزايا iOS CallKit وعيوبه

**المزايا:**

- **التكامل الأصلي:** يوفّر CallKit واجهة اتصال أصلية تجعل مكالمات VoIP لا تختلف عن المكالمات العادية.
- **تجربة المستخدم:** يحسّن تجربة المستخدم باستخدام واجهة المكالمات المألوفة في iOS.
- **العمل في الخلفية:** يتيح لتطبيقات VoIP استقبال المكالمات حتى عندما لا يكون التطبيق نشطًا.**العيوب:**- **التعقيد:** قد يكون دمج CallKit معقدًا، ويتطلب فهمًا عميقًا لإطاري CallKit وPushKit معًا.
- **القيود:** قد تكون بعض الوظائف محدودة مقارنة بتطبيقات الاتصال الهاتفي التقليدية.

# استكشاف الأخطاء وإصلاحها
أصبح لدينا الآن ما يكفي من الكود للرد على مكالمات VoIP باستخدام Twilio في تطبيق iOS. وإذا كانت المكالمات لا تصل لسبب ما، فإليكم بعض الأفكار لاستكشاف المشكلة:
\- إذا كانت المكالمة تظهر في واجهة النظام لكنكم لا تسمعون أي صوت، فتأكدوا من إعداد جلسة الصوت بشكل صحيح، ومن أنكم طلبتم إذن الميكروفون وحصلتم عليه
\- إذا كانت المكالمة لا تصل إطلاقًا، فتأكدوا من نجاح تسجيل الـ access token، وراجعوا الشهادة كذلك. ففي لوحة تحكم Twilio حيث ترفعون الشهادة، يوجد مربع يمكنكم تحديده أو إلغاء تحديده باسم "sandbox testing only". وإذا كنتم تختبرون في وضع debug في iOS، فيجب تحديد هذا المربع.
\- راجعوا إعدادات الخادم وتأكدوا من ضبط قيمة PUSH_CREDENTIAL_SID.
**المصادر
\- فيديو WWDC 2016 بعنوان Enhancing VoIP Apps with CallKit:** https://developer.apple.com/videos/play/wwdc2016/230/ (ننصح به بشدة)**\- دليل Twilio Voice QuickStart:** https://github.com/twilio/voice-quickstart-ios
\- **توثيق CallKit:** https://developer.apple.com/documentation/callkit?language=objc

---

Contact OOZOU: hello@oozou.com · +66 2544 0000 · https://oozou.com/ar/contact

More for AI agents: https://oozou.com/llms.txt · every page has a markdown twin at its URL plus ".md"
