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

Swift中@escaping闭包仍使用过期值问题求助

SwiftUI中CustomHStack与TappableView结合时闭包捕获过期值的问题解决

问题描述

在SwiftUI应用中,自定义了封装UITapGestureRecognizer的TappableView(接收@escaping类型的action闭包),同时自定义了CustomHStack结构体。当两者结合使用并触发状态变更(比如交换数组元素)后,视图能正确重绘,但action闭包会使用过期的数值。单独使用这两个结构体时无此问题,且已确认所有需要的场景都使用了@escaping。

复现步骤:

  1. 运行提供的测试代码
  2. 点击“Swap items”按钮交换数组元素
  3. 点击顶部的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()
        }
    }
}

修复效果

完成上述修改后,再次运行代码:

  1. 点击“Swap items”交换元素,视图会正确更新显示最新数值
  2. 点击CustomHStack + TappableView区域内的按钮,打印出的数值会是当前最新的元素值,不再使用过期数据

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 05:24:56