Callkit

作者 dpearson26998d90fd121a26无许可证1.1K 个星标收录于 2026年10月8日更新于 2026年10月8日仓库2个月前更新

Implement VoIP calling with CallKit and PushKit. Use when building incoming/outgoing call flows, registering for VoIP push notifications, configuring CXProvider and CXCallController, handling call actions, coordinating audio sessions, or creating Call Directory extensions for caller ID and call blocking.

AI 生成的概览

指导使用 CallKit 和 PushKit 实现 iOS VoIP 通话,涵盖通话流程、推送注册、音频会话和来电识别。

功能
该技能提供使用 CallKit 和 PushKit 构建 iOS VoIP 通话功能的参考指南和代码模式。内容涵盖 provider 配置、呼入与呼出通话流程、VoIP 推送注册与上报规则、音频会话协调,以及用于来电识别和拦截的 Call Directory 扩展。此外还列出常见错误和审查清单。
适用场景
适用于构建呼入或呼出通话流程、注册 VoIP 推送通知、配置 CXProvider 或 CXCallController、处理通话操作、协调音频会话,或创建 Call Directory 扩展的场景。
运行要求
需要具备 CallKit 和 PushKit 的 iOS 项目;该技能仅为说明文档,不包含脚本。

CallKit

Build VoIP calling features that integrate with the native iOS call UI using CallKit and PushKit. Covers incoming/outgoing call flows, VoIP push registration, audio session coordination, and call directory extensions.

Contents

Setup

Project Configuration

  1. Enable the Voice over IP background mode in Signing & Capabilities
  2. Add the Push Notifications capability
  3. For call directory extensions, add a Call Directory Extension target

Key Types

TypeRole
CXProviderReports calls to the system, receives call actions
CXCallControllerRequests call actions (start, end, hold, mute)
CXCallUpdateDescribes call metadata (caller name, video, handle)
CXProviderDelegateHandles system call actions and audio session events
PKPushRegistryRegisters for and receives VoIP push notifications
PKVoIPPushMetadataiOS 26.4+ metadata that says whether a VoIP push must be reported

Provider Configuration

Create a single CXProvider at app launch and keep it alive for the app lifetime. Configure it with a CXProviderConfiguration that describes your calling capabilities.

swift
import CallKit
/// CXProvider dispatches all delegate calls to the queue passed to `setDelegate(_:queue:)`./// The `let` properties are initialized once and never mutated, making this type/// safe to share across concurrency domains despite @unchecked Sendable.final class CallManager: NSObject, @unchecked Sendable {    static let shared = CallManager()
    let provider: CXProvider    let callController = CXCallController()
    private override init() {        let config = CXProviderConfiguration()        config.localizedName = "My VoIP App"        config.supportsVideo = true        config.maximumCallsPerCallGroup = 1        config.maximumCallGroups = 2        config.supportedHandleTypes = [.phoneNumber, .emailAddress]        config.includesCallsInRecents = true
        provider = CXProvider(configuration: config)        super.init()        provider.setDelegate(self, queue: nil)    }}

Incoming Call Flow

When a required VoIP call push arrives, report the incoming call to CallKit immediately. The system displays the native call UI. You must report required calls before the PushKit completion handler returns -- failure to do so causes the system to terminate your app.

swift
func reportIncomingCall(    uuid: UUID,    handle: String,    hasVideo: Bool) async throws {    let update = CXCallUpdate()    update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)    update.hasVideo = hasVideo    update.localizedCallerName = "Jane Doe"
    try await withCheckedThrowingContinuation {        (continuation: CheckedContinuation<Void, Error>) in        provider.reportNewIncomingCall(            with: uuid,            update: update        ) { error in            if let error {                continuation.resume(throwing: error)            } else {                continuation.resume()            }        }    }}

Handling the Answer Action

Implement CXProviderDelegate to respond when the user answers:

swift
extension CallManager: CXProviderDelegate {    func providerDidReset(_ provider: CXProvider) {        // End all calls, reset audio    }
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {        // Prepare audio, then fulfill only after the call is actually ready        configureAudioSession()        connectToCallServer(callUUID: action.callUUID) { success in            if success {                action.fulfill()            } else {                provider.reportCall(                    with: action.callUUID,                    endedAt: Date(),                    reason: .failed                )                action.fail()            }        }    }
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {        disconnectFromCallServer(callUUID: action.callUUID)        action.fulfill()    }}

Outgoing Call Flow

Use CXCallController to request an outgoing call. The system routes the request through your CXProviderDelegate.

swift
func startOutgoingCall(handle: String, hasVideo: Bool) {    let uuid = UUID()    let handle = CXHandle(type: .phoneNumber, value: handle)    let startAction = CXStartCallAction(call: uuid, handle: handle)    startAction.isVideo = hasVideo
    let transaction = CXTransaction(action: startAction)    callController.request(transaction) { error in        if let error {            print("Failed to start call: \(error)")        }    }}

Delegate Methods for Outgoing Calls

swift
extension CallManager {    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {        configureAudioSession()        // Begin connecting to server        provider.reportOutgoingCall(            with: action.callUUID,            startedConnectingAt: Date()        )
        connectToServer(callUUID: action.callUUID) {            provider.reportOutgoingCall(                with: action.callUUID,                connectedAt: Date()            )        }        action.fulfill()    }}

PushKit VoIP Registration

Register for VoIP pushes at every app launch and send token changes to your server. For iOS 13 SDK+ apps, every report-required VoIP call push must be reported before PushKit completion using CallKit, or LiveCommunicationKit for apps built on that framework. On iOS 26.4+, PKVoIPPushMetadata.mustReport is the gate: true means report before completion; false means no CallKit or LiveCommunicationKit report is required. Missing a required report before completion can terminate the app, and repeated failures may stop VoIP delivery.

PathReport decisionCompletion timing
iOS 26.4+ mustReport == trueReport with CallKit or LiveCommunicationKitAfter report callback
iOS 26.4+ mustReport == falseNo CallKit/LiveCommunicationKit report requiredAfter local handling
Older delegateiOS 13 SDK+ treats VoIP call pushes as report-requiredAfter report callback
swift
import PushKit
final class PushManager: NSObject, PKPushRegistryDelegate {    let registry: PKPushRegistry
    override init() {        registry = PKPushRegistry(queue: .main)        super.init()        registry.delegate = self        registry.desiredPushTypes = [.voIP]    }
    func pushRegistry(        _ registry: PKPushRegistry,        didUpdate pushCredentials: PKPushCredentials,        for type: PKPushType    ) {        let token = pushCredentials.token            .map { String(format: "%02x", $0) }            .joined()        // Send token to your server        sendTokenToServer(token)    }
    @available(iOS 26.4, *)    func pushRegistry(        _ registry: PKPushRegistry,        didReceiveIncomingVoIPPushWith payload: PKPushPayload,        metadata: PKVoIPPushMetadata,        withCompletionHandler completion: @escaping @Sendable () -> Void    ) {        guard metadata.mustReport else {            completion()            return        }        handleIncomingVoIPPush(payload, completion: completion)    }
    // Keep the older callback for iOS 26.0-26.3 and older deployment targets.    func pushRegistry(        _ registry: PKPushRegistry,        didReceiveIncomingPushWith payload: PKPushPayload,        for type: PKPushType,        completion: @escaping () -> Void    ) {        guard type == .voIP else {            completion()            return        }
        handleIncomingVoIPPush(payload, completion: completion)    }
    private func handleIncomingVoIPPush(        _ payload: PKPushPayload,        completion: @escaping () -> Void    ) {        let callUUID = UUID()        let handle = payload.dictionaryPayload["handle"] as? String ?? "Unknown"
        Task {            do {                try await CallManager.shared.reportIncomingCall(                    uuid: callUUID,                    handle: handle,                    hasVideo: false                )            } catch {                // Call was filtered by DND or block list            }            completion()        }    }}

Server-side VoIP pushes should use a short lifetime: set apns-expiration to 0 or only a few seconds. After the initial push wakes the app, send hangups and call-detail changes over the app-server connection instead of sending more VoIP pushes.

Audio Session Coordination

CallKit owns the audio activation boundary: start media only in provider(_:didActivate:), and stop or tear it down in provider(_:didDeactivate:) and reset paths.

swift
extension CallManager {    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {        // Audio session is now active -- start audio engine / WebRTC        startAudioEngine()    }
    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {        // Audio session deactivated -- stop audio engine        stopAudioEngine()    }
    func configureAudioSession() {        let session = AVAudioSession.sharedInstance()        do {            try session.setCategory(                .playAndRecord,                mode: .voiceChat,                options: [.allowBluetooth, .allowBluetoothA2DP]            )        } catch {            print("Audio session configuration failed: \(error)")        }    }}

Call Directory Extension and Manager

Use Call Directory for preloaded caller ID/blocking, not per-call API lookup. The extension loads sorted bulk data in beginRequest(with:); the main app uses CXCallDirectoryManager to check enabled status, open Call Blocking & Identification settings when disabled, and reload after data changes. Store CXCallDirectoryPhoneNumber as country code plus digits in ascending order (for example 18005551234), not a formatted string.

swift
import CallKit
final class CallDirectoryHandler: CXCallDirectoryProvider {    override func beginRequest(        with context: CXCallDirectoryExtensionContext    ) {        if context.isIncremental {            addOrRemoveIncrementalEntries(to: context)        } else {            addAllEntries(to: context)        }        context.completeRequest()    }
    private func addAllEntries(        to context: CXCallDirectoryExtensionContext    ) {        // Country code + digits, sorted in ascending order        let blockedNumbers: [CXCallDirectoryPhoneNumber] = [            18005551234, 18005555678        ]        for number in blockedNumbers {            context.addBlockingEntry(                withNextSequentialPhoneNumber: number            )        }
        let identifiedNumbers: [(CXCallDirectoryPhoneNumber, String)] = [            (18005551111, "Local Pizza"),            (18005552222, "Dentist Office")        ]        for (number, label) in identifiedNumbers {            context.addIdentificationEntry(                withNextSequentialPhoneNumber: number,                label: label            )        }    }}

Main-App Manager: Status, Settings, Reload

swift
let manager = CXCallDirectoryManager.sharedInstancemanager.getEnabledStatusForExtension(withIdentifier: extensionID) { status, _ in    guard status == .enabled else {        manager.openSettings { _ in } // Call Blocking & Identification        return    }    manager.reloadExtension(withIdentifier: extensionID) { _ in }}

Check getEnabledStatusForExtension(...) before assuming the extension is active, use openSettings(...) for Call Blocking & Identification when disabled, and call reloadExtension(...) after data changes. Route APNs auth-key rotation and normal remote-notification setup to push-notifications.

Common Mistakes

MistakeFix
Required VoIP push is treated as data-onlyApply the version-specific report rule, report before completion, then invoke the PushKit completion handler.
Answer action is fulfilled before media/server readinessKeep it pending while connecting; fulfill on readiness or fail and report .failed.
Media starts before provider(_:didActivate:)Prepare earlier if needed, but start only after activation and stop on deactivation/reset.
An action path never calls fulfill() or fail()Give success, cancellation, timeout, and network-error paths one terminal action.
Token refresh is ignoredSend every didUpdate pushCredentials token to the server.
Call Directory performs per-call networkingPreload sorted entries and reload the extension.

Review Checklist

  • VoIP background mode enabled in capabilities
  • Single CXProvider instance created at app launch and retained
  • CXProviderDelegate set before reporting any calls
  • iOS 26.4+ PushKit path reports when mustReport is true and may skip when false
  • iOS 13 SDK+ PushKit VoIP call pushes report to CallKit before completion
  • VoIP APNs requests use apns-expiration of 0 or only a few seconds
  • Hangups and detail updates use the app-server connection after the initial push
  • action.fulfill() or action.fail() called for every provider delegate action
  • CXAnswerCallAction fulfilled only after the call server/media connection is ready
  • Audio engine started only after provider(_:didActivate:) callback
  • Audio engine stopped in provider(_:didDeactivate:) callback
  • Audio session category set to .playAndRecord with .voiceChat mode
  • VoIP push token sent to server on every didUpdate pushCredentials callback
  • PKPushRegistry created at every app launch (not lazily)
  • Call Directory data is preloaded, not fetched per incoming call
  • CXCallDirectoryPhoneNumber documented as country calling code + digits
  • CXCallDirectoryManager names status check, reload, and settings-opening APIs
  • CXCallUpdate populated with localizedCallerName and remoteHandle
  • Outgoing calls report startedConnectingAt and connectedAt timestamps
  • iOS 26 call translation keeps upstream audio active during mute
  • Encrypted metadata filtering mentions the notification service extension entitlement

References

来源与署名

来源:dpearson2699/swift-ios-skills位于skills/callkit提交8d90fd1

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架

更多来自 dpearson2699/swift-ios-skills 的技能

Widgetkit

dpearson2699

指导实现、审查和改进 iOS、iPadOS、watchOS 与 CarPlay 上的 WidgetKit 小组件和控制项。

Software Development1.1K2个月前更新

Weatherkit

dpearson2699

指导 iOS 开发者使用 WeatherService 获取 WeatherKit 预报、警报与署名信息。

Software Development1.1K2个月前更新

Vision Framework

dpearson2699

Implement computer vision features including text recognition (OCR), face detection, barcode scanning, image segmentation, object tracking, and document scanning in iOS apps. Covers both the modern Swift-native Vision API (iOS 18+) and legacy VNRequest patterns, VisionKit DataScannerViewController for live camera scanning, and CoreMLRequest/VNCoreMLRequest for custom model inference. Use when adding OCR, barcode scanning, face detection, or custom Core ML model inference with Vision.

待分类1.1K2个月前更新

Tipkit

dpearson2699

Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.

待分类1.1K2个月前更新

Tabletopkit

dpearson2699

指导使用 TabletopKit 在 visionOS 上构建多人空间桌游,涵盖棋具、座位、动作与 RealityKit 渲染。

Software Development1.1K2个月前更新

Swiftui Webkit

dpearson2699

指导在 iOS 26 及更高版本的 SwiftUI 应用中使用 WebKit for SwiftUI 嵌入和控制网页内容。

Software Development1.1K2个月前更新