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

iOS SwiftUI问题:ViewBuilder中@State变量读取旧值

解决SwiftUI中@ViewBuilder闭包捕获@State旧值的问题

问题现象

自定义LoadingView通过@ViewBuilder闭包渲染加载完成后的内容,当父视图的@State变量在加载完成前更新,闭包渲染时仍会使用初始值;但加载完成后更新@State则能正常刷新。使用@ObservedObject可解决该问题,但希望保留@State的使用方式。

问题原因

LoadingView初始化时,@ViewBuilder闭包会捕获当前父视图(值类型)的@State变量快照。当父视图的@State更新时,父视图会生成新的实例,旧闭包捕获的仍是旧实例的@State值;而LoadingView因视图身份未变,其@State var loaded会被SwiftUI托管保留,直到加载完成后才调用闭包,此时若闭包未更新,就会使用旧的@State值。

解决方案

方案1:通过@Binding传递状态到LoadingView

将父视图的@State以绑定形式传递给LoadingView,让闭包直接从绑定中获取最新值,避免捕获旧的实例快照:

struct PresenterView: View {
    enum PresentationMode {
        case small, big
    }
    
    @State private var presentationMode: PresentationMode = .small
    
    var body: some View {
        LoadingView(presentationMode: $presentationMode) { message, mode in
            switch mode {
            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 presentationMode: Binding<PresenterView.PresentationMode>
    let content: (String, PresenterView.PresentationMode) -> Content
    
    init(presentationMode: Binding<PresenterView.PresentationMode>, @ViewBuilder content: @escaping (String, PresenterView.PresentationMode) -> Content) {
        self.presentationMode = presentationMode
        self.content = content
    }
    
    var body: some View {
        Group {
            if loaded {
                content("Data", presentationMode.wrappedValue)
            } else {
                Text("Data is still loading")
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                print("Data is loaded")
                self.loaded = true
            }
        }
    }
}

方案2:强制闭包随父视图重绘更新

无需修改LoadingView结构,利用@State变化触发父视图body重绘的机制,确保LoadingView的@ViewBuilder闭包被重新生成。若存在SwiftUI视图优化导致闭包未更新,可给LoadingView添加id标识强制刷新:

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)")
            }
        }
        .id(presentationMode) // 添加id,确保状态变化时LoadingView重新初始化
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                print("Presentation mode is changed, before data is loaded")
                self.presentationMode = .big
            }
        }
    }
}

// LoadingView代码保持不变
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
            }
        }
    }
}

效果验证

两种方案都能确保加载完成后,闭包获取@State的最新值,正确显示"Big presentation of: Data"。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 09:51:29