SwiftUI中ForEach更新时仅执行特定次数的问题排查
问题描述
点击「Go Back」和「Save & Continue」按钮更新currentIndex时,LeftView中的ForEach出现异常行为:
- 初始渲染时执行5次
let _ = print(index) - 后续修改
currentIndex时,每次仅执行两次不同索引的循环
BulletView内的打印日志:
初始状态:
Customer Information true System Information false Payment false Add Template false Receipt false
currentIndex增至1时:
Customer Information true System Information false
currentIndex增至2时:
System Information false Payment false
相关代码:
fileprivate let titlesList = ["Customer Information", "System Information", "Payment", "Add Template", "Receipt"] struct InstallationView: View { @ObservedObject private var viewModel = InstallationViewModel() var body: some View { NavigationView { GeometryReader { metrics in HStack(alignment: .center, spacing: 0) { LeftView(currentIndex: $viewModel.currentIndex) .frame(width: metrics.size.width * 0.35) .frame(maxHeight: .infinity) RightView(currentIndex: $viewModel.currentIndex) .frame(width: metrics.size.width * 0.65) .frame(maxHeight: .infinity) } } }.navigationBarHidden(true).navigationViewStyle(StackNavigationViewStyle()) } } fileprivate struct LeftView: View { @Binding var currentIndex: Int var body: some View { VStack(alignment: .center, spacing: 0) { ForEach(0..<titlesList.count, id: \.self) { index in let _ = print(index) BulletView(count: index + 1, title: titlesList[index], isActive: (currentIndex == index), showTopSeparator: (index != 0), showBottomSeparator: (index != titlesList.count - 1)) } }.padding(EdgeInsets.init(top: 0, leading: 100, bottom: 0, trailing: 0)) } } fileprivate struct RightView: View { @Binding var currentIndex: Int private let edges = EdgeInsets.init(top: 0, leading: 0, bottom: 20, trailing: 20) private let leftEdges = EdgeInsets.init(top: 0, leading: 10, bottom: 0, trailing: 0) @Environment(\.presentationMode) var presentationMode var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { Text(titlesList[currentIndex]) .font(Font(UIFont.boldSystemFont(ofSize: 24))) .foregroundColor(Color.black) .padding(EdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 0)) containedView() HStack { Button(action: { currentIndex -= 1 }) { Text("Go Back") .frame(width: 200, height: 70) .font(Font(UIFont.boldSystemFont(ofSize: 24))) .foregroundColor(.white) .background(Color(Constants.Colors.main)) .cornerRadius(5) } Spacer() Button(action: { currentIndex += 1 }) { Text("Save & Continue") .frame(width: 200, height: 70) .font(Font(UIFont.boldSystemFont(ofSize: 24))) .foregroundColor(.white) .background(Color(Constants.Colors.main)) .cornerRadius(5) } } .padding(edges) }.padding(EdgeInsets.init(top: 80, leading: 0, bottom: 0, trailing: 0)) } } func containedView() -> AnyView { switch currentIndex { case 0: return AnyView(CustomerView()) case 1: return AnyView(SystemView()) case 2: return AnyView(PaymentView()) default: return AnyView(CustomerView()) } } } struct InstallationView_Previews: PreviewProvider { static var previews: some View { InstallationView().previewInterfaceOrientation(.landscapeLeft) } }
原因分析
这是SwiftUI差异化视图更新机制导致的正常行为:
- SwiftUI的核心优化逻辑是仅重新渲染依赖状态发生变化的视图,以此避免不必要的性能消耗。
- 对于
BulletView,只有isActive参数直接依赖currentIndex。当currentIndex变化时,仅两个BulletView的状态会改变:- 上一个选中项的
isActive从true变为false - 新选中项的
isActive从false变为true
- 上一个选中项的
- 其余
BulletView的所有参数(count/title/showTopSeparator/showBottomSeparator)均未发生变化,因此SwiftUI不会重新渲染这些视图,自然也不会执行视图内部的print语句,以及ForEach中对应索引的print(index)。
解决方案
如果需要让所有BulletView都响应currentIndex变化(比如存在其他依赖currentIndex的UI逻辑),可以采用以下两种方式:
- 显式添加依赖参数:给
BulletView新增一个接收currentIndex的参数(即使暂时不使用),强制SwiftUI感知到该视图依赖currentIndex:
BulletView(count: index + 1, title: titlesList[index], isActive: (currentIndex == index), showTopSeparator: (index != 0), showBottomSeparator: (index != titlesList.count - 1), currentIndex: currentIndex) // 新增依赖参数
- 强制视图重建(不推荐,影响性能):给
LeftView添加.id(currentIndex)修饰符,让currentIndex变化时整个LeftView重新构建:
LeftView(currentIndex: $viewModel.currentIndex) .frame(width: metrics.size.width * 0.35) .frame(maxHeight: .infinity) .id(viewModel.currentIndex) // 强制视图重建
如果只是调试阶段的打印需求,该现象属于SwiftUI的正常优化,无需额外处理。
内容的提问来源于stack exchange,提问作者Muhammad Umar
相关产品推荐
相关产品推荐

