简体   繁体   English

iOS - SwiftUI - viewBuilder 中的@State 变量值具有旧值

[英]iOS - SwiftUI - @State variable value in viewBuilder has old value

I am using a LoadingView view to show an activity indicator before data is loaded.我正在使用LoadingView视图在加载数据之前显示活动指示器。 The LoadingView receives a @ViewBuilder closure to render the content after the data is loaded. LoadingView接收一个@ViewBuilder闭包以在数据加载后呈现内容。

The @ViewBuilder closure captures a @State variable, but if this state was updated, the @ViewBuilder gets the old value for the state. @ViewBuilder闭包捕获一个@State变量,但如果此 state 已更新, @ViewBuilder将获取 state 的旧值。

An easy code snippet is worth thousand words:一个简单的代码片段值一千字:

struct PresenterView: View {
    
    enum PresentationMode {
        case small, big
    }
    
    @State private var presentationMode: PresentationMode = .small
    
    var body: some View {
        LoadingView() { message in
            switch self.presentationMode {
            case .small:
                Text("Small presentation of: \(message)")
            case .big:
                Text("Big presentation of: \(message)")
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                print("Presentation mode is changed, before data is loaded")
                self.presentationMode = .big
            }
        }
    }
}


struct LoadingView<Content: View>: View {
    
    @State var loaded = false
    
    let content: (String) -> Content
    init(@ViewBuilder content: @escaping (String) -> Content) {
        self.content = content
    }
    
    var body: some View {
        Group {
            if loaded {
                content("Data")
            }
            else {
                Text("Data is still loading")
            }
        }
        .onAppear() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                print("Data is loaded")
                self.loaded = true
            }
        }
    }
}

In this example, the presentation mode is changed to .big before the data is loaded.在此示例中,在加载数据之前将演示模式更改为.big When the @ViewBuilder is rendered, it still uses the initial .small value.@ViewBuilder被渲染时,它仍然使用初始的.small值。

Some findings: If presentation mode is changed to .big after the data is loaded, the @ViewBuilder view gets updated properly.一些发现:如果在加载数据后将显示模式更改为.big@ViewBuilder视图会正确更新。 It is my understanding, that internally the @ViewBuilder view does only bind to the state after it has been presented.据我了解, @ViewBuilder视图在内部仅在呈现后才绑定到 state。 Still, it should be possible for the @ViewBuilder to get the current value of the state.不过,@ViewBuilder 应该可以获取@ViewBuilder的当前值。 After all, when the @ViewBuilder is presented, the binding to the state works correctly.毕竟,当@ViewBuilder出现时,与 state 的绑定工作正常。

Using an @ObservedObject to wrap the presentationMode works fine, but I would like to avoid this, in order to use @State reliable.使用@ObservedObject来包装presentationMode工作正常,但我想避免这种情况,以便使用@State可靠。

Thanks for the help!谢谢您的帮助!

You are not using the content in LoadingView until the data is loaded, so SwiftUI fails to recreate the LoadingView when presentationMode is updated.在加载数据之前,您不会使用LoadingView中的content ,因此 SwiftUI 在更新presentationMode时无法重新创建LoadingView There are a number of ways to fix this.有很多方法可以解决这个问题。

Mark content as capturing presentationModecontent标记为捕获presentationMode

If you mark the content of LoadingView as capturing presentationMode , then LoadingView will get updated when presentationMode changes.如果您将LoadingViewcontent标记为捕获presentationMode ,则LoadingView将在presentationMode更改时更新。

struct ContentView: View {
    
    enum PresentationMode {
        case small, big
    }
    
    @State private var presentationMode: PresentationMode = .small
    
    var body: some View {
        LoadingView() { [presentationMode] message in
            switch presentationMode {
            case .small:
                Text("Small presentation of: \(message)")
            case .big:
                Text("Big presentation of: \(message)")
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                print("Presentation mode is changed, before data is loaded")
                self.presentationMode = .big
            }
        }
    }
}

Use content from the start从一开始就使用content

Because your LoadingView isn't using the content when presentationMode is changed, SwiftUI fails to recreate the LoadingView so it is stuck with the original value of presentationMode .因为您的LoadingViewpresentationMode更改时没有使用content ,所以 SwiftUI 无法重新创建LoadingView ,因此它卡在了presentationMode的原始值上。

If you change your LoadingView to use the content but make it invisible until loaded, then the LoadingView will get updated when presentationMode changes:如果您将LoadingView更改为使用content但使其在加载之前不可见,则LoadingView将在presentationMode更改时更新:

var body: some View {
    ZStack {
        content("Data")
            .opacity(loaded ? 1 : 0)
        if !loaded {
            Text("Data is still loading")
        }
    }
    .onAppear() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.loaded = true
        }
    }
}

Enable LoadingView to control loaded State启用LoadingView以控制loaded的 State

If you only want the real content to be rendered when the data is loaded, you can wrap that content in if loaded { } and pass loaded as a Binding to the LoadingView , and then LoadingView can set loaded = true when the data is loaded, then the PresenterView will reload LoadingView with the real content which will use the latest versions of the @State variables:如果您只想在加载数据时呈现真实content ,您可以将该内容包装在if loaded { }中并将loaded作为Binding传递给LoadingView ,然后LoadingView可以在加载数据时设置loaded = true ,然后PresenterView将使用最新版本的@State变量重新加载LoadingView的真实content

struct PresenterView: View {
    
    enum PresentationMode {
        case small, big
    }
    
    @State private var presentationMode: PresentationMode = .small
    @State private var loaded = false
    
    var body: some View {
        LoadingView(loaded: $loaded) { message in
            if loaded {
                let _ = print("presenting now")
                switch self.presentationMode {
                case .small:
                    Text("Small presentation of: \(message)")
                case .big:
                    Text("Big presentation of: \(message)")
                }
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                print("Presentation mode is changed, before data is loaded")
                self.presentationMode = .big
            }
        }
        
    }
}

struct LoadingView<Content: View>: View {
    @Binding var loaded: Bool
    let content: (String) -> Content
    init(loaded: Binding<Bool>, @ViewBuilder content: @escaping (String) -> Content) {
        self._loaded = loaded
        self.content = content
    }
    
    var body: some View {
        ZStack {
            content("Data")
            if !loaded {
                Text("Data is still loading")
            }
        }
        .onAppear() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.loaded = true
            }
        }
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM