You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS端flutter_callkit_incoming插件:接听呼叫无法自动唤起App求助

问题描述

使用flutter_callkit_incoming插件实现跨平台呼叫通知,Android端运行正常;iOS端能接收通知,但App处于后台/终止状态时,点击接听无法自动唤起App,手动打开App后呼叫可正常启动,期望点击接听直接唤起App。已在WidgetsBinding.instance.addPostFrameCallback中添加checkAndNavigationCallingPage()方法,使用插件最新版本,附AppDelegate.swift代码。


解决方案

1. 补充CallKit代理实现,处理呼叫动作回调

当前AppDelegate缺少CXProviderDelegate协议实现,这是iOS唤起App并响应CallKit接听/挂断动作的核心逻辑。修改代码如下:

import UIKit
import Flutter
import flutter_local_notifications
import flutter_callkit_incoming
import CallKit
import AVFAudio
import PushKit

@main
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CXProviderDelegate {
    // 全局CallKit Provider实例
    var callKitProvider: CXProvider?

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // 原有代码保持不变
        FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
          GeneratedPluginRegistrant.register(with: registry)
        }
        
        if #available(iOS 10.0, *) {
          UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
        }
        
        GeneratedPluginRegistrant.register(with: self)

        // 初始化CallKit Provider
        let callKitConfig = CXProviderConfiguration(localizedName: "你的App名称")
        callKitConfig.supportsVideo = true
        callKitConfig.maximumCallsPerCallGroup = 1
        callKitConfig.supportedHandleTypes = [.generic]
        callKitProvider = CXProvider(configuration: callKitConfig)
        callKitProvider?.delegate = self
        
        // Setup VOIP
        let mainQueue = DispatchQueue.main
        let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [PKPushType.voIP]
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // MARK: - CXProviderDelegate 实现
    // 激活音频会话
    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        do {
            try audioSession.setActive(true)
        } catch {
            print("激活音频会话失败: \(error)")
        }
    }

    // 处理接听动作
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        // 通过自定义Scheme唤起App
        guard let url = URL(string: "your-app-scheme://call-answer") else {
            action.fail()
            return
        }
        UIApplication.shared.open(url, options: [:])
        
        // 通知插件处理接听事件
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.answerCall(action.callUUID.uuidString)
        action.fulfill()
    }

    // 处理挂断动作
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.endCall(action.callUUID.uuidString)
        action.fulfill()
    }

    // 原有PushRegistry相关方法保持不变
    func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
        print(credentials.token)
        let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
        print(deviceToken)
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        print("didInvalidatePushTokenFor")
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
    }
    
    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 ?? UUID().uuidString
        let nameCaller = (payload.dictionaryPayload["params"] as? [String: Any])? ["body"] as? String ?? ""
        let appName = (payload.dictionaryPayload["params"] as? [String: Any])? ["title"] as? String ?? ""
        let handle = payload.dictionaryPayload["handle"] as? String ?? "generic"
        let isVideo = payload.dictionaryPayload["isVideo"] as? Int ?? 1
        let duration = payload.dictionaryPayload["duration"] as? Int ?? 30000
        let ringtonePath = payload.dictionaryPayload["ringtonePath"] as? String ?? "system_ringtone_default"
        
        var args: NSDictionary = ["id": id, "nameCaller": nameCaller, "appName": appName, "handle": handle, "type": isVideo, "duration": duration, "ringtonePath": ringtonePath]
        let data = flutter_callkit_incoming.Data(args: args)
        data.extra =  payload.dictionaryPayload["params"] as? NSDictionary ?? [:]
        data.iconName = ""
        data.type = 1;
        
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)
        // 移除延迟,直接调用completion
        completion()
    }
}

2. 配置App自定义URL Scheme

在Xcode中为App添加自定义Scheme,确保CallKit能通过URL唤起App:

  1. 打开项目Info标签页
  2. 找到URL Types,点击+添加新类型
  3. Identifier填写任意标识,URL Schemes填写自定义Scheme(比如your-app-scheme,需和代码中保持一致)

3. Flutter端监听CallKit事件

确保Flutter端监听CALL_ACCEPT事件,在App唤起后自动导航到呼叫页面:

FlutterCallkitIncoming.onEvent.listen((event) {
  switch (event!.name) {
    case EventAction.ACTION_CALL_ACCEPT:
      // 导航到呼叫页面
      checkAndNavigationCallingPage();
      break;
    // 其他事件处理(如挂断、拒绝)
    default:
      break;
  }
});

4. 检查iOS权限配置

确保项目开启必要权限:

  • 在Capabilities中开启Push Notifications和Background Modes,勾选Voice over IP
  • 在Info.plist中添加推送权限描述:
<key>NSUserNotificationUsageDescription</key>
<string>需要推送通知接收呼叫</string>
<key>NSPushNotificationUsageDescription</key>
<string>需要VoIP推送接收呼叫</string>

内容的提问来源于stack exchange,提问作者Djai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:01:07