什么是“闭包捕获值不更新”?
在 SwiftUI 的 .sheet、.alert、.onAppear、.animation 等等中使用闭包(也叫 trailing closure),Swift 会在“写这个闭包时”捕获当时的值,而不是“点下去的时候”的值。

简单示例:
struct ContentView: View {
@State private var index = 0
@State private var showSheet = false
var body: some View {
VStack {
Button("点我") {
index = 3
showSheet = true
}
.sheet(isPresented: $showSheet) {
Text("当前 index 是 \(index)")
}
}
.padding()
}
}
在这个代码中,可以看到当点击按钮时,sheet显示的index是0,而不是设置的3。

这就是闭包捕获值不更新现象,index值是最开始的值,而不是更新后的值。
为什么会发生?
SwiftUI会预先创建.sheet的内容,并不是每次点击都会重新创建。
也就是说,.sheet的闭包会提前以值类型保存下来,里面的数值是当时的快照。

这里的关键点是,.sheet是一个“逃逸闭包”,SwiftUI会在某些时候只捕获它第一次展示时的上下文值,这就导致看到的index不是点击时的最新值,而是旧值。
这里的“值不更新”的问题,也就是闭包捕获值不变。
正确做法
当需要在.sheet,.alert等UI控件中动态传值时,应该使用对应的方法传值:
import SwiftUI
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
@State private var index: Int? = nil
var body: some View {
VStack {
Button("点我") {
index = 3
}
.sheet(item: $index) { value in
Text("当前 index 是 \(value)")
}
}
.padding()
}
}

在这里使用的是sheet(item:)方法将index值传递进来。当使用sheet(item:)时,监听的不再是布尔值,而是监听item的变化本身。
当item从nil变成某只值(比如3)时,SwiftUI会重新创建Sheet View,并将item的当前值作为参数传入,所以闭包中的value就是最新的值。
这里还新增了一个Int扩展,用于遵循Identifiable协议。
如果不添加扩展,sheet(item:)绑定的过程中会提示报错:
Instance method 'sheet(item:onDismiss:content:)' requires that 'Int' conform to 'Identifiable'

报错原因为,SwiftUI要求item必须遵循Identifiable协议的类型,而Int并不是Identifiable。
相关文章
1、Swift深入理解闭包捕获机制:https://fangjunyu.com/2024/11/28/swift%e6%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3%e9%97%ad%e5%8c%85%e6%8d%95%e8%8e%b7%e6%9c%ba%e5%88%b6/
2、Swift深入理解闭包表达式:https://fangjunyu.com/2024/11/26/swift%e6%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3%e9%97%ad%e5%8c%85%e8%a1%a8%e8%be%be%e5%bc%8f/
3、Swift闭包的三种形式以及理解闭包的嵌套函数:https://fangjunyu.com/2024/10/23/swift%e9%97%ad%e5%8c%85%e7%9a%84%e4%b8%89%e7%a7%8d%e5%bd%a2%e5%bc%8f%e4%bb%a5%e5%8f%8a%e7%90%86%e8%a7%a3%e9%97%ad%e5%8c%85%e7%9a%84%e5%b5%8c%e5%a5%97%e5%87%bd%e6%95%b0/