SwiftUI数据变更时重绘整个视图,需仅更新指定子视图
问题排查与优化方案
问题根源
- 父视图强制重绘:
MyReferralsList作为父视图直接持有@ObservedObject类型的MyReferralsListVM,当data这个Published属性更新时,SwiftUI 会触发父视图的body重新计算,导致所有子视图(包括ScrollingViewForBtns)被重绘。 - 子视图内部创建 ViewModel:
ScrollingViewForBtns内部直接初始化了ReferralsStatusViewModel,每次父视图重绘时,这个子视图都会重建新的 ViewModel 实例,进而触发自身重绘。 - Equatable 实现无效:给父视图实现
Equatable无法阻止重绘——因为ObservedObject的Published属性变更会优先触发父视图重绘,不会先执行==比较逻辑。
优化步骤
1. 拆分 ViewModel 依赖,避免子视图内部初始化
将 ScrollingViewForBtns 的 ViewModel 提升到父视图初始化,通过参数传入,确保其生命周期与父视图绑定,不会随父视图重绘而重建:
// 修改 ScrollingViewForBtns struct ScrollingViewForBtns: View { @ObservedObject var referralsStatusVM: ReferralsStatusViewModel @Binding var selectedButtonIndex: Int var statusBtnTapped: (Int) -> Void var body: some View { ScrollView(.horizontal, showsIndicators: false){ HStack(spacing: 16){ ForEach(referralsStatusVM.buttons.indices, id:\.self) { index in StatusButton(btnText: referralsStatusVM.buttons[index].name, btnAction: { selectedButtonIndex = index statusBtnTapped(referralsStatusVM.buttons[index].id) }, isSelected: Binding( get: { selectedButtonIndex == index }, set: { _ in })) } } .padding(.vertical,10) .padding(.horizontal,2) } } }
2. 让列表子视图直接观察 ViewModel
将 ReferralListSubView 改为接收 ViewModel 而非直接传 data,让它仅监听 data 的变更,避免父视图传递数据时触发不必要的重绘:
// 修改 ReferralListSubView struct ReferralListSubView: View { @ObservedObject var viewModel: MyReferralsListVM var body: some View { List{ ForEach(viewModel.data, id:\.id) { referral in NavigationLink(destination: ReferralDetailView(referral: referral)) { ReferralsCells(referral: referral) } .frame(height: 81.0) .padding(.horizontal) .background(Color.white) .cornerRadius(6) .shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2) .listRowBackground(Color.clear) } } .listRowSeparator(.hidden) .listStyle(PlainListStyle()) .background(Color.white) } }
3. 重构父视图,移除无效的 Equatable 实现
删除 MyReferralsList 的 Equatable 实现,同时统一管理所有 ViewModel 的初始化:
// 修改 MyReferralsList struct MyReferralsList: View { @ObservedObject var myReferralsListViewModel: MyReferralsListVM @ObservedObject var referralsStatusVM: ReferralsStatusViewModel @State private var selectedStatus: Int = 0 private var organizationId: Int = 0 init(organizationId: Int, statusId: Int? = 0){ self.organizationId = organizationId self.myReferralsListViewModel = MyReferralsListVM(referralsService:ReferralsService(networking: Networking.shared), organizationId: organizationId) self.referralsStatusVM = ReferralsStatusViewModel(referralService: ReferralsService(networking: Networking.shared)) } var body: some View { ZStack(alignment: .top) { Color.white VStack(alignment: .leading, spacing: 16.0) { ScrollingViewForBtns( referralsStatusVM: referralsStatusVM, selectedButtonIndex: $selectedStatus, statusBtnTapped: { id in if selectedStatus == 0 { myReferralsListViewModel.fetchData(id: organizationId) }else{ myReferralsListViewModel.fetchReferralForStatus(statusId: id) } } ) .equatable() // 配合下面的 Equatable 实现,阻止无必要重绘 ReferralListSubView(viewModel: myReferralsListViewModel) Spacer() } .padding(.vertical, 14) .padding(.horizontal, 15) } .foregroundColor(Color("textGray")) } } // 给 ScrollingViewForBtns 实现 Equatable,确保参数不变时不重绘 extension ScrollingViewForBtns: Equatable { static func == (lhs: ScrollingViewForBtns, rhs: ScrollingViewForBtns) -> Bool { lhs.selectedButtonIndex == rhs.selectedButtonIndex && lhs.referralsStatusVM.buttons == rhs.referralsStatusVM.buttons } }
额外优化建议
- 确保
Referral类型正确实现Equatable,让ForEach可以精准判断哪些列表项需要更新。 - 如果项目支持 iOS14+,将父视图中初始化 ViewModel 的
@ObservedObject改为@StateObject,进一步稳定 ViewModel 的生命周期。
内容的提问来源于stack exchange,提问作者Umair Khan
相关产品推荐
相关产品推荐

