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) } }
解决建议
修正Podfile的Flavor映射
当前Podfile的project配置未明确prod flavor的构建类型,修改为:project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, 'Prod' => :release, # 新增prod flavor对应关系 }强制清理并重新安装依赖
在CodeMagic构建脚本中执行完整的清理命令:flutter clean rm -rf ios/Pods ios/Podfile.lock ios/Runner.xcworkspace flutter pub get cd ios && pod install --repo-update检查Xcode中Prod Flavor的依赖配置
确认Xcode项目中,Prod目标的Link Binary With Libraries包含flutter_callkit_incoming的框架,且Build Settings中的Framework Search Paths正确指向插件的iOS目录。验证Git插件的iOS模块完整性
检查Git仓库的main分支,确认ios目录存在且包含完整的插件代码(比如flutter_callkit_incoming.podspec文件是否正确配置)。调整AppDelegate导入方式
如果模块名存在歧义,尝试使用大小写匹配的导入语句:// 替换原导入语句 import FlutterCallkitIncoming
内容的提问来源于stack exchange,提问作者Esraa Gamal
相关产品推荐
相关产品推荐

