这是我想做的:
1.具有更改本地State变量的SwiftUI视图
1.点击按钮时,将该变量传递给应用程序的其他部分
然而,由于某种原因,即使我更新了状态变量,当它被传递到下一个视图时,它也没有得到更新。
下面的一些示例代码显示了这个问题:
struct NumberView: View {
@State var number: Int = 1
@State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
如果你点击"更改数字",它会将本地状态更新为99。但是当我创建另一个视图并将其作为参数传递时,它会显示1而不是99。
注意事项:
- 如果你取消对
Text("\(number)")
的注解,它就可以工作。但是这不应该是必要的。 - 如果你让
SomeView
使用绑定,它也可以工作。但是对于我的应用程序,这不起作用。我的实际用例是一个"选择游戏选项"视图。然后,我将创建一个非SwiftUI游戏视图,并希望将这些选项作为参数传入。所以,我不能仅仅因为这个bug就在我的游戏代码中使用绑定。我只想捕获用户输入的内容,并使用这些数据创建一个Parameters对象。 - 如果你把它变成
navigationDestination
而不是fullScreenCover
,它也可以工作。
3条答案
按热度按时间bnlyeluc1#
视图是一个结构体,因此它的属性是不可变的,因此视图不能更改它自己的属性。这就是为什么从视图体内部更改名为
number
的属性需要用**@State
属性 Package 器注解此属性的原因。感谢Swift和SwiftUI,透明的读写回调允许看到的值发生变化。因此,在调用fullScreenCover()
时,不能将number
作为SomeView()
的参数传递,而是传递对number
的引用。为了系统地调用回调:$number
**.由于构造结构SomeView时不再传递整数,因此该结构中名为number
的属性的类型不能再为整数,而必须是对整数的引用(即绑定):为此使用@Binding
注解。因此,将
SomeView(number: number)
替换为SomeView(number: $number)
,将let number: Int
替换为@Binding var number: Int
即可完成此工作。下面是正确的源代码:
毕竟,说要获得一个有效的源代码,他们是一个小技巧,还没有解释到现在:如果您只是在源代码中将
Text("Change Number")
替换为Text("Change Number \(number)")
,而不使用$
引用或@Binding
关键字,你会发现问题也自动解决了!不需要在SomeView
中使用@binding
!这是因为SwiftUI在构建视图树时进行了优化。如果它知道显示的视图发生了变化(不仅是其属性),它将使用更新的@State
值计算视图。将number
添加到按钮标签会使SwiftUI跟踪number
状态属性的更改,并且它现在更新其缓存值以显示文本按钮标签,因此此新值将正确用于创建SomeView。所有这些可能会被视为奇怪的事情,而仅仅是由于SwiftUI中的优化。苹果没有完全解释它如何实现构建视图树的优化,在WWDC活动期间提供了一些信息,但源代码没有公开。因此,您需要严格遵循基于@State
和@Binding
的设计模式,以确保整个事情像它应该的那样工作。所有这一切再次说明,有人可能会说苹果说,如果一个子视图只想访问一个值,你不必使用
@Binding
来传递一个值给这个子视图:* 与任何也需要访问的子视图共享状态,无论是直接进行只读访问,还是作为读写访问的绑定 *(https://developer.apple.com/documentation/swiftui/state)。这是正确的,但苹果在同一篇文章中说,你需要 * 将[state]放在需要访问值的视图层次结构中的最高视图中 *。对于苹果来说,需要访问值意味着你需要它来显示视图,而不仅仅是做其他对屏幕没有影响的计算。正是这种解释让苹果在需要更新NumberView时优化state属性的计算,例如在计算Text("Change Number \(number)")
行的内容时。你可能会发现这真的很棘手。但有一种方法可以理解这一点:取你写的初始代码,去掉var number: Int = 1
前面的@State
.为了编译它,你需要把这一行从结构体内部移到外部,比如在你的源文件的第一行,就在import声明之后.你会看到它工作了!这是因为你不需要这个值来显示NumberView.因此,将值设置得更高以构建名为SomeView的视图是完全法律的的。2注意,这里您不想更新SomeView,因此没有边框效果。3但是如果您必须更新SomeView,它将不起作用。下面是最后一个技巧的代码:
**这就是为什么您一定要遵循
@State
和@Binding
设计模式的原因,考虑到如果您在一个视图中声明一个状态,而该视图不使用它来显示其内容,您应该在子视图中将此状态声明为@Binding,即使这些子视图不需要对此状态进行更改。**使用@State
的最佳方法是在需要它显示某些内容的最高视图中声明它:永远不要忘记@State
必须在拥有这个变量的视图中声明;创建拥有变量但不必使用它来显示其内容的视图是反模式。nx7onnlm2#
由于
number
不是在body中读取的,SwiftUI的依赖跟踪会检测到它。你可以像这样给予它一个微调:现在,每当
number
改变时,将使用更新的number
值创建一个新的闭包。顺便说一下,[number] in
语法被称为“捕获列表”,请在这里阅读。wko9yo5t3#
Nathan Tannar通过另一个渠道给了我这个解释,我认为这是我问题的症结所在。看起来这确实是SwiftUI的怪异之处,因为它知道何时以及如何基于状态更新视图。谢谢Nathan!
这是因为数字在视图的主体中不是“读”的。SwiftUI很聪明,因为它只在视图的依赖关系改变时才触发视图更新。为什么这会导致fullScreenCover修饰符出现问题,是因为它捕获了主体的@转义闭包。这意味着它在封面出现之前不会被读取。由于它没有被读取,所以当@State改变时,视图主体不会被重新计算。你可以通过在视图体中设置断点来验证这一点。因为视图体不会被重新计算,所以@escaping闭包永远不会被重新捕获,因此它将保存原始值的副本。
顺便说一句,你会发现,一旦你第一次展示封面,然后关闭,后续的展示将正确更新。可以说,这似乎是一个SwiftUI错误,fullScreenCover可能不应该是@escaping。你可以通过阅读正文中的数字来解决,或者用类似这样的东西 Package 修饰符,由于此处未捕获@escaping目的地,因此将在视图主体评估中读取该编号。