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

Flutter iOS生产环境构建失败:找不到flutter_callkit_incoming模块

Flutter iOS Prod Flavor构建失败:flutter_callkit_incoming模块找不到

问题现象

执行以下命令构建iOS IPA时:

flutter build ipa --release --export-options-plist /Users/builder/export_options.plist --flavor prod -t lib/main_prod.dart

终端报错:

💪 Building with sound null safety 💪
Archiving com.mindscape.app...
Automatically signing iOS for device deployment using specified development team in Xcode project: 9PF4M6PSKX
Running pod install...                                             5.2s  
Running Xcode build...  
Xcode archive done. 78.6s  
Failed to build iOS app  
Swift Compiler Error (Xcode): No such module 'flutter_callkit_incoming'  
 /Users/builder/clone/ios/Runner/AppDelegate.swift:3:7
Encountered error while archiving for device.

注意:使用--release --flavor dev可正常构建,仅prod flavor出现此问题

环境信息

  • Flutter版本:3.7.10
  • flutter_callkit_incoming版本:2.0.0+1(来自Git仓库)
  • 构建环境:CodeMagic(本地设备为Windows)

已执行步骤

  • flutter clean
  • flutter pub upgrade --major-versions(返回flutter_callkit_incoming 2.0.0+1 from git)
  • pod install(返回Installing flutter_callkit_incoming (0.0.1))
  • Xcode构建(触发上述错误)

相关配置文件

pubspec.yaml 相关片段

...
# call kit
flutter_callkit_incoming:
  git:
    url: https://github.com/Mindsacpe/callkit-plugin.git
    ref: main # branch name
    version: 2.0.0+1
...

pubspec.lock 相关片段

packages:
  flutter_callkit_incoming:
    dependency: "direct main"
    description:
      path: "."
      ref: main
      resolved-ref: "66c127445afbd69813485a00e572d1e091c70ed6"
      url: "https://github.com/Mindsacpe/callkit-plugin.git"
    source: git
    version: "2.0.0+1"
...

Podfile

# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
deployment_target = '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
    config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = deployment_target

    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
            '$(inherited)',

            ## dart: PermissionGroup.calendar
            'PERMISSION_EVENTS=1',

            ## dart: PermissionGroup.reminders
            # 'PERMISSION_REMINDERS=1',

            ## dart: PermissionGroup.contacts
            # 'PERMISSION_CONTACTS=1',

            ## dart: PermissionGroup.camera
            'PERMISSION_CAMERA=1',

            ## dart: PermissionGroup.microphone
            'PERMISSION_MICROPHONE=1',

            ## dart: PermissionGroup.speech
            # 'PERMISSION_SPEECH_RECOGNIZER=1',

            ## dart: PermissionGroup.photos
            'PERMISSION_PHOTOS=1',

            ## dart: PermissionGroup.audio session
            'AUDIO_SESSION_MICROPHONE=1',

            ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
            # 'PERMISSION_LOCATION=1',

            ## dart: PermissionGroup.notification
            # 'PERMISSION_NOTIFICATIONS=1',

            ## dart: PermissionGroup.mediaLibrary
            'PERMISSION_MEDIA_LIBRARY=1',

            ## dart: PermissionGroup.sensors
            # 'PERMISSION_SENSORS=1',

            ## dart: PermissionGroup.bluetooth
            # 'PERMISSION_BLUETOOTH=1',

            ## dart: PermissionGroup.appTrackingTransparency
            # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1',

            ## dart: PermissionGroup.criticalAlerts
            # 'PERMISSION_CRITICAL_ALERTS=1'
          ]
    end
  end
end

AppDelegate.swift

import UIKit
import PushKit
import Flutter
import flutter_callkit_incoming


@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate {

    var methodOff = "UserOffline_Ios"
    var methodOn = "UserOnline_Ios"
    var channel = "com_mindscape_ios"
    var flutterChannel  = FlutterMethodChannel()

  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]

      if #available(iOS 10.0, *) {
          // For iOS 10 display notification (sent via APNS)
          UNUserNotificationCenter.current().delegate = self
          let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
          UNUserNotificationCenter.current().requestAuthorization(
                  options: authOptions,
                  completionHandler: {_, _ in })
      } else {
          let settings: UIUserNotificationSettings =
          UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
          application.registerUserNotificationSettings(settings)
      }
      application.registerForRemoteNotifications()

      let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
         flutterChannel = FlutterMethodChannel(name: channel, binaryMessenger: controller.binaryMessenger)

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground.
     override func applicationWillTerminate(_ application: UIApplication) {
             print("applicationWillTerminate called")
         self.flutterChannel.invokeMethod(methodOff,arguments: "")
    }
    
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    override func applicationWillEnterForeground(_ application: UIApplication) {
        print("applicationWillResume called")
        self.flutterChannel.invokeMethod(methodOn,arguments: "")
    }
    
//    // Call back from Recent history
//    override func application(_ application: UIApplication,
//                              continue userActivity: NSUserActivity,
//                              restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
//
//        guard let handleObj = userActivity.handle else {
//            return false
//        }
//
//        guard let isVideo = userActivity.isVideo else {
//            return false
//        }
//        let nameCaller = handleObj.getDecryptHandle()["nameCaller"] as? String ?? ""
//        let handle = handleObj.getDecryptHandle()["handle"] as? String ?? ""
//        let data = flutter_callkit_incoming.Data(id: UUID().uuidString, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
//        //set more data...
//        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.startCall(data, fromPushKit: true)
//
//        return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
//    }
//
    // Handle updated push credentials
    func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {

        let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
        print(deviceToken)
        //Save deviceToken to your server
        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 ?? ""
        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)
        data.extra = payload.dictionaryPayload["extra"] as? NSDictionary ?? NSDictionary()
        //set more data
        //data.iconName = ...
        //data.....
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)
    }
}

解决建议

  1. 修正Podfile的Flavor映射
    当前Podfile的project配置未明确prod flavor的构建类型,修改为:

    project 'Runner', {
      'Debug' => :debug,
      'Profile' => :release,
      'Release' => :release,
      'Prod' => :release, # 新增prod flavor对应关系
    }
    
  2. 强制清理并重新安装依赖
    在CodeMagic构建脚本中执行完整的清理命令:

    flutter clean
    rm -rf ios/Pods ios/Podfile.lock ios/Runner.xcworkspace
    flutter pub get
    cd ios && pod install --repo-update
    
  3. 检查Xcode中Prod Flavor的依赖配置
    确认Xcode项目中,Prod目标的Link Binary With Libraries包含flutter_callkit_incoming的框架,且Build Settings中的Framework Search Paths正确指向插件的iOS目录。

  4. 验证Git插件的iOS模块完整性
    检查Git仓库的main分支,确认ios目录存在且包含完整的插件代码(比如flutter_callkit_incoming.podspec文件是否正确配置)。

  5. 调整AppDelegate导入方式
    如果模块名存在歧义,尝试使用大小写匹配的导入语句:

    // 替换原导入语句
    import FlutterCallkitIncoming
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 14:39:50