Flutter集成iOS原生模块时,Swift中安全传递UINavigationController以避免数据竞争的方案
首先,我们来拆解你遇到的问题:警告的核心来自两个点——@Sendable闭包的类型约束要求和UIKit对象的线程安全规则,再加上原代码中获取导航控制器的逻辑有冗余,可能导致潜在的不一致性。下面是一步步的解决方案:
问题根源分析
- @Sendable约束警告:你的
completion闭包被标记为@Sendable,但传递的FlutterResult闭包默认没有显式标记为@Sendable,Swift编译器会怀疑它可能携带非线程安全的引用。 - UIKit线程安全:
UINavigationController是UIKit对象,所有访问和操作必须在主线程执行(你已经用DispatchQueue.main.async保证了这一点,但原代码的导航控制器获取逻辑有重复,可能导致意外)。 - 闭包捕获风险:隐式捕获函数参数可能导致生命周期不确定的引用,虽然
FlutterMethodCall是值类型(拷贝捕获安全),但显式捕获更清晰可控。
正确实现方案
1. 优化导航控制器获取逻辑,统一主线程安全获取
简化重复的window检查逻辑,适配iOS 13+的多场景应用架构,确保只在主线程操作UIKit对象。
2. 满足@Sendable约束,显式标记线程安全的闭包
确保传递给@Sendable闭包的所有参数都符合类型要求,尤其是FlutterResult闭包。
3. 显式捕获异步闭包变量,避免隐式引用风险
修改后的完整代码
private func initialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { getNavigationControllerLegacy(call: call, result: result) { methodCall, flutterResult, navigation in guard let nav = navigation else { flutterResult(FlutterError(code: "NO_NAV", message: "Navigation controller not found", details: nil)) return } // 所有UI操作已在主线程执行(completion在main.async中调用) let liveness = Liveness(methodCall, result: flutterResult) liveness.viewController = nav liveness.execute() } } private func getNavigationControllerLegacy( call: FlutterMethodCall, result: @escaping @Sendable FlutterResult, completion: @escaping @Sendable (FlutterMethodCall, @escaping @Sendable FlutterResult, UINavigationController?) -> Void ) { // 显式捕获值类型的call和标记为@Sendable的result,避免隐式捕获风险 DispatchQueue.main.async { [call, result] in var navigationController: UINavigationController? // 统一获取当前活跃的Key Window及根控制器(适配多场景架构) if let keyWindow = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .flatMap({ $0.windows }) .first(where: { $0.isKeyWindow }), let rootVC = keyWindow.rootViewController { // 两种场景:根控制器本身是导航控制器,或根控制器被导航控制器包裹 navigationController = (rootVC as? UINavigationController) ?? rootVC.navigationController } // 传递安全捕获的变量和导航控制器 completion(call, result, navigationController) } }
关键修改点解释
@Sendable标记FlutterResult:
我们将result参数和completion闭包中的FlutterResult都标记为@escaping @Sendable。Flutter的FlutterResult闭包本身是线程安全的(Flutter引擎会处理跨线程调用的同步),显式标记可以消除编译器警告,同时符合@Sendable闭包的类型要求。显式捕获异步闭包变量:
在DispatchQueue.main.async中显式捕获call和result,避免隐式捕获函数上下文的其他变量(虽然这里没有,但显式捕获更清晰,也能确保我们使用的是当前函数参数的正确拷贝/引用)。简化导航控制器获取逻辑:
合并了原代码中重复的window检查逻辑,通过connectedScenes正确获取当前活跃的UIWindowScene,再找到Key Window,最后从根控制器中获取导航控制器。这种方式更符合iOS 13+的多场景应用架构,避免了旧版UIApplication.shared.windows的潜在问题。主线程安全保证:
所有访问UINavigationController的逻辑都在DispatchQueue.main.async中执行,完全符合UIKit的线程安全要求——UIKit对象的所有操作必须在主线程进行,这样就彻底避免了跨线程访问UI对象导致的数据竞争。
额外注意事项
- FlutterResult的调用时机:虽然Flutter允许在任何线程调用
FlutterResult,但如果你的Liveness.execute()中涉及UI操作,最好确保result的回调也在主线程执行(不过Flutter会自动将结果同步到Dart线程,所以一般不需要额外处理)。 - 导航控制器的生命周期:通过
isKeyWindow保证了获取的UINavigationController是当前活跃的,避免使用已经被释放或不在前台的window。
内容来源于stack exchange

