当作为非绑定参数传递时,为什么SwiftUI状态不更新?

8wigbo56  于 2023-02-21  发布在  Swift
关注(0)|答案(3)|浏览(142)

这是我想做的:
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,它也可以工作。
bnlyeluc

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即可完成此工作。
下面是正确的源代码:

import SwiftUI

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 {
    @Binding var number: Int

    var body: some View {
        Text("\(number)")
    }
}

毕竟,说要获得一个有效的源代码,他们是一个小技巧,还没有解释到现在:如果您只是在源代码中将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,它将不起作用。
下面是最后一个技巧的代码:

import SwiftUI

// number is declared outside the views!
var number: Int = 1

struct NumberView: View {
    // no more state variable named number!
    // No more modification: the following code is exactly yours!

    @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)")
    }
}

**这就是为什么您一定要遵循@State@Binding设计模式的原因,考虑到如果您在一个视图中声明一个状态,而该视图不使用它来显示其内容,您应该在子视图中将此状态声明为@Binding,即使这些子视图不需要对此状态进行更改。**使用@State的最佳方法是在需要它显示某些内容的最高视图中声明它:永远不要忘记@State必须在拥有这个变量的视图中声明;创建拥有变量但不必使用它来显示其内容的视图是反模式。

nx7onnlm

nx7onnlm2#

由于number不是在body中读取的,SwiftUI的依赖跟踪会检测到它。你可以像这样给予它一个微调:

.fullScreenCover(isPresented: $showNumber) { [number] in

现在,每当number改变时,将使用更新的number值创建一个新的闭包。顺便说一下,[number] in语法被称为“捕获列表”,请在这里阅读。

wko9yo5t

wko9yo5t3#

Nathan Tannar通过另一个渠道给了我这个解释,我认为这是我问题的症结所在。看起来这确实是SwiftUI的怪异之处,因为它知道何时以及如何基于状态更新视图。谢谢Nathan!
这是因为数字在视图的主体中不是“读”的。SwiftUI很聪明,因为它只在视图的依赖关系改变时才触发视图更新。为什么这会导致fullScreenCover修饰符出现问题,是因为它捕获了主体的@转义闭包。这意味着它在封面出现之前不会被读取。由于它没有被读取,所以当@State改变时,视图主体不会被重新计算。你可以通过在视图体中设置断点来验证这一点。因为视图体不会被重新计算,所以@escaping闭包永远不会被重新捕获,因此它将保存原始值的副本。
顺便说一句,你会发现,一旦你第一次展示封面,然后关闭,后续的展示将正确更新。可以说,这似乎是一个SwiftUI错误,fullScreenCover可能不应该是@escaping。你可以通过阅读正文中的数字来解决,或者用类似这样的东西 Package 修饰符,由于此处未捕获@escaping目的地,因此将在视图主体评估中读取该编号。

struct FullScreenModifier<Destination: View>: ViewModifier {
    @Binding var isPresented: Bool
    @ViewBuilder var destination: Destination
    func body(content: Content) -> some View {
        content
            .fullScreenCover(isPresented: $isPresented) {
                destination
            }
    }
}

相关问题