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
相关产品推荐
相关产品推荐

