iOS CallKit only works on a real device. The CallKit framework is not available on the iOS Simulator. Do not test incoming call functionality in the Simulator.
Configure Info.plist
Open ios/Runner/Info.plist and add the required background modes. At minimum, voip and remote-notification are required:<key>UIBackgroundModes</key>
<array>
<string>voip</string>
<string>remote-notification</string>
<string>processing</string> <!-- add this if needed -->
</array>
Enable Voice over IP capability in Xcode
Open your project in Xcode and navigate to:Xcode Project → Your Target → Signing & CapabilitiesClick + Capability and add Background Modes. Then check Voice over IP in the list of background modes.This capability must be explicitly enabled in Xcode in addition to the Info.plist entry. Skipping this step means your app will not receive VoIP pushes in the background.
Add a CallKit icon
Add a CallKitLogo image set to your ios/Runner/Images.xcassets folder in Xcode. This image is displayed inside the system CallKit UI when an incoming call arrives.The default icon name used by the plugin is CallKitLogo. You can change this by setting iconName in IOSParams:ios: IOSParams(
iconName: 'CallKitLogo', // matches the name in Images.xcassets
),
Configure AppDelegate.swift
Your AppDelegate must conform to PKPushRegistryDelegate and CallkitIncomingAppDelegate. This wires up VoIP push handling and call event callbacks.Open ios/Runner/AppDelegate.swift and replace or update it with the following:import UIKit
import CallKit
import AVFAudio
import PushKit
import Flutter
import flutter_callkit_incoming
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Setup VOIP
let mainQueue = DispatchQueue.main
let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]
// Use if using WebRTC
// RTCAudioSession.sharedInstance().useManualAudio = true
// RTCAudioSession.sharedInstance().isAudioEnabled = false
// Add for Missed call notification
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Handle updated push credentials
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
print(credentials.token)
let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
// Save deviceToken to your server
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
print("didInvalidatePushTokenFor")
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
}
// Handle incoming VoIP pushes
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
print("didReceiveIncomingPushWith")
guard type == .voIP else { return }
let id = payload.dictionaryPayload["id"] as? String ?? ""
let nameCaller = payload.dictionaryPayload["nameCaller"] as? String ?? ""
let handle = payload.dictionaryPayload["handle"] as? String ?? ""
let isVideo = payload.dictionaryPayload["isVideo"] as? Bool ?? false
let data = flutter_callkit_incoming.Data(id: id, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
// Set more data as needed
// data.extra = ["user": "abc@123", "platform": "ios"]
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true) {
completion()
}
}
// Called when the user accepts a call
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any]
print("LOG: onAccept")
// Perform your API call here, then call action.fulfill() when done
// (e.g., when WebRTC connection is established)
action.fulfill()
}
// Called when the user declines a call
func onDecline(_ call: Call, _ action: CXEndCallAction) {
let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any]
print("LOG: onDecline")
// Perform your API call here, then call action.fulfill() when done
action.fulfill()
}
// Called when the user ends a call
func onEnd(_ call: Call, _ action: CXEndCallAction) {
let json = ["action": "END", "data": call.data.toJSON()] as [String: Any]
print("LOG: onEnd")
// Perform your API call here, then call action.fulfill() when done
action.fulfill()
}
// Called when the incoming call times out (missed)
func onTimeOut(_ call: Call) {
let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any]
print("LOG: onTimeOut")
}
// Called when the audio session is activated (e.g., call accepted)
func didActivateAudioSession(_ audioSession: AVAudioSession) {
// Use if using WebRTC:
// RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
// RTCAudioSession.sharedInstance().isAudioEnabled = true
}
// Called when the audio session is deactivated (e.g., call ended)
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {
// Use if using WebRTC:
// RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
// RTCAudioSession.sharedInstance().isAudioEnabled = false
}
}
Set up missed call notifications
To show missed call notifications and handle the Call back action while the app is in the foreground, add two UNUserNotificationCenter delegate methods to your AppDelegate.These are already included in the full AppDelegate.swift example above. The key methods are:// Show notification when app is in the foreground
override func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
@escaping (UNNotificationPresentationOptions) -> Void) {
CallkitNotificationManager.shared.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler)
}
// Handle callback action tapped in a missed call notification
override func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == CallkitNotificationManager.CALLBACK_ACTION {
let data = response.notification.request.content.userInfo as? [String: Any]
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.sendCallbackEvent(data)
}
completionHandler()
}
This delegates foreground notification display to CallkitNotificationManager and routes the Call back tap to the Flutter event stream as a actionCallCallback event.