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

SwiftUI数据变更时重绘整个视图,需仅更新指定子视图

问题排查与优化方案

问题根源

  1. 父视图强制重绘:MyReferralsList 作为父视图直接持有 @ObservedObject 类型的 MyReferralsListVM,当 data 这个 Published 属性更新时,SwiftUI 会触发父视图的 body 重新计算,导致所有子视图(包括 ScrollingViewForBtns)被重绘。
  2. 子视图内部创建 ViewModel:ScrollingViewForBtns 内部直接初始化了 ReferralsStatusViewModel,每次父视图重绘时,这个子视图都会重建新的 ViewModel 实例,进而触发自身重绘。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 16:07:31