Swift中@escaping闭包仍使用过期值问题求助
问题描述
在SwiftUI应用中,自定义了封装UITapGestureRecognizer的TappableView(接收@escaping类型的action闭包),同时自定义了CustomHStack结构体。当两者结合使用并触发状态变更(比如交换数组元素)后,视图能正确重绘,但action闭包会使用过期的数值。单独使用这两个结构体时无此问题,且已确认所有需要的场景都使用了@escaping。
复现步骤:
- 运行提供的测试代码
- 点击“Swap items”按钮交换数组元素
- 点击顶部的
CustomHStack + TappableView区域内的任意按钮,会发现打印出错误的旧数值
问题根源分析
1. CustomHStack的实现缺陷
当前CustomHStack在初始化阶段就将所有子视图映射为[AnyView]并存储为常量,后续通过索引遍历展示。当数据源items.items发生变化时,CustomHStack不会重新执行子视图的生成逻辑,导致子视图(包括TappableView)还是初始化时的实例,闭包捕获的仍是旧的数据源值。
2. TappableViewRepresentable未更新闭包
TappableView内部的TappableViewRepresentable(UIViewRepresentable)的updateUIView方法为空,当父视图状态变化时,SwiftUI会触发updateUIView,但未更新Coordinator中的action闭包,导致手势触发时执行的还是旧的闭包。
解决方案
修复CustomHStack
修改CustomHStack的实现,不再提前存储子视图数组,而是直接在body中基于数据源动态生成子视图,确保数据源变化时重新生成所有子视图:
public struct CustomHStack<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable { private let data: Data private let content: (Data.Element) -> Content init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) { self.data = data self.content = content } public var body: some View { HStack { ForEach(data) { element in content(element) } } } }
如果需要支持自定义ID的KeyPath,可以调整为:
public struct CustomHStack<Data: RandomAccessCollection, ID: Hashable, Content: View>: View { private let data: Data private let id: KeyPath<Data.Element, ID> private let content: (Data.Element) -> Content init(_ data: Data, id: KeyPath<Data.Element, ID>, @ViewBuilder content: @escaping (Data.Element) -> Content) { self.data = data self.id = id self.content = content } // 针对Identifiable元素的默认初始化 init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) where Data.Element: Identifiable { self.init(data, id: \.id, content: content) } public var body: some View { HStack { ForEach(data, id: id) { element in content(element) } } } }
修复TappableViewRepresentable
在updateUIView方法中更新Coordinator的action闭包,确保状态变化时闭包始终是最新的:
private struct TappableViewRepresentable: UIViewRepresentable { let action: () -> Void init(action: @escaping () -> Void) { self.action = action } typealias UIViewType = UIView func makeCoordinator() -> Coordinator { Coordinator(action: action) } func makeUIView(context: Context) -> UIView { let view = UIView() let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(sender:))) view.addGestureRecognizer(tapGestureRecognizer) return view } func updateUIView(_ uiView: UIView, context: Context) { // 更新Coordinator中的action为最新闭包 context.coordinator.action = action } class Coordinator { var action: () -> Void // 改为var允许更新 init(action: @escaping () -> Void) { self.action = action } @objc func handleTap(sender: UITapGestureRecognizer) { guard sender.state == .ended else { return } action() } } }
修复效果
完成上述修改后,再次运行代码:
- 点击“Swap items”交换元素,视图会正确更新显示最新数值
- 点击
CustomHStack + TappableView区域内的按钮,打印出的数值会是当前最新的元素值,不再使用过期数据
内容的提问来源于stack exchange,提问作者John Cashew

