SwiftUI中NavigationView导致@State属性值异常清空是什么原因?
问题描述
我正在学习斯坦福大学iOS开发课程,在课程示例基础上做自主拓展,尝试实现卡片入场动画效果:通过@State修饰的Set<String>类型变量dealt存储已展示卡片的ID,在content视图的.onAppear回调中遍历所有卡片,将卡片ID逐个插入dealt集合,判断时仅当isUndealt(card)返回true时显示透明占位,否则渲染卡片视图并配置缩放入场过渡动画。
相关代码
import SwiftUI struct EmojiMemoryGameView: View { @ObservedObject var game: EmojiMemoryGame var body: some View { NavigationView { content .navigationTitle(game.themeName) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .bottomBar) { Text("Score: \(game.score)") .font(.title3) .fontWeight(.semibold) Spacer() shuffleButton } } } .navigationViewStyle(.stack) } @State private var dealt = Set<String>() private func deal(_ card: EmojiMemoryGame.Card) { print("Before insert: \(dealt)") print("Inserting \(card.id)") dealt.insert(card.id) let bah = dealt print("After insert: \(dealt)") } private func isUndealt(_ card: EmojiMemoryGame.Card) -> Bool { print("dealt: \(dealt)") let bah = dealt return !dealt.contains(card.id) } var content: some View { VStack { themeSelector.frame(maxWidth: .infinity) AspectVGrid(items: game.cards, aspectRatio: 2/3) { card in if isUndealt(card) || (card.isMatched && !card.isFaceUp) { Color.clear } else { CardView(card: card) .padding(4) .transition(AnyTransition.asymmetric(insertion: .scale, removal: .opacity)) .onTapGesture { withAnimation { game.choose(card) } } } } .onAppear { withAnimation { for card in game.cards { deal(card) } } } .foregroundColor(game.themeColor) .padding(.horizontal) } } ...
运行日志
Before insert: [] Inserting 4AE58783-5F32-4359-8E88-300D73F1AA89 After insert: ["4AE58783-5F32-4359-8E88-300D73F1AA89"] Before insert: ["4AE58783-5F32-4359-8E88-300D73F1AA89"] Inserting B82B98B1-47AC-4836-BE01-05FEFAC083E5 After insert: ["4AE58783-5F32-4359-8E88-300D73F1AA89", "B82B98B1-47AC-4836-BE01-05FEFAC083E5"] ... dealt: [] dealt: [] ...
异常表现
日志显示deal()方法中所有ID都成功插入dealt集合,但后续isUndealt()方法读取dealt时始终返回空集合,动画无法正常触发。
奇怪的是如果我移除外层的NavigationView,直接在body中渲染content视图,代码改成:
var body: some View { VStack { content shuffleButton } .padding() }
动画就能正常运行,日志也显示dealt集合的值可以正常保留。
疑问
为什么外层视图容器会影响@State属性的工作逻辑?第一版代码的问题出在哪里?
解答
这个问题的核心原因和SwiftUI的视图渲染机制、@State的生命周期有关:
NavigationView的初始渲染流程会触发两次子视图的求值:第一次是为了计算导航栏、工具栏等容器组件的布局参数,这次求值生成的临时视图实例会被直接丢弃;第二次才会生成最终渲染到屏幕上的视图实例。@State属性的存储是绑定到对应视图实例的,第一次临时视图的onAppear触发时修改的是临时实例的dealt属性,当临时视图被丢弃后,你修改的dealt值也一起被销毁了。第二次渲染生成的正式视图实例的dealt属性会回到初始的空集合,且正式视图的content不会再次触发onAppear回调(SwiftUI会认为onAppear已经被调用过),所以最终你读到的dealt始终是空的。- 你移走
NavigationView之后,视图只会渲染一次,onAppear修改的dealt就是最终渲染的视图实例的属性,所以逻辑正常。
修复方案
可以任选其一:
- 把
onAppear回调挂载到外层的NavigationView上,而不是内部的content上,避免临时视图触发的状态修改被丢弃 - 把
dealt属性从@State改成@StateObject包装的独立数据模型,@StateObject的生命周期不受视图轻量重建的影响,状态会被持久保留 - 给
content添加稳定的唯一标识,避免视图实例被重建:content.id("stable_content_id")
内容的提问来源于stack exchange,提问作者Hayden BL
相关产品推荐
相关产品推荐

