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

SwiftUI:更新ViewModel属性时避免整个视图重渲染

核心问题

能否在更新ViewModel中的单个@Published属性时,避免触发整个观察它的视图重新加载?

问题场景

子视图包含Lottie动画,同时通过Firebase实时数据库监听云端对象变化。采用MVVM架构时,每当Firebase更新任意属性,整个视图就会重新加载,导致动画重新播放。需要持续监听Firebase实现其他UI动态更新,但不希望任意属性变化都触发视图重绘(进而重启动画)。

已尝试@EnvironmentObject、@StateObject,也通过!=判断避免本地数据无变化时从Firebase更新,了解过Equatable协议,但不确定是否适用此场景,询问是否可以仅针对单个属性更新做出视图响应。

解决方案

1. 仅监听单个@Published属性(推荐)

SwiftUI允许视图只观察ViewModel的特定@Published属性,而非整个ObservableObject。通过将子视图的依赖从整个ViewModel改为单个属性,就能避免无关属性更新导致的重绘。

修改LottieView,直接接收lottieToShow作为值传递,而非整个ViewModel:

// 子视图修改后
struct LottieView: View {
  let lottieToShow: String
    
  var body: some View {
    Lottie(name: lottieToShow, loopMode: .playOnce)
      .frame(width: 250, height: 250)
  }
}

// 主视图传递单个属性
struct ContentView: View {
  @StateObject var someViewModel: SomeViewModel

  var body: some View {
    LottieView(lottieToShow: someViewModel.lottieToShow)
  }
}

这样只有lottieToShow更新时,LottieView才会重绘,someOtherProperty变化不会影响它。

2. 使用Equatable优化视图重绘

如果必须传递整个ViewModel给子视图,可以让LottieView遵循Equatable协议,仅在lottieToShow变化时才重绘。

struct LottieView: View, Equatable {
  @ObservedObject var someViewModel: SomeViewModel
    
  static func == (lhs: LottieView, rhs: LottieView) -> Bool {
    // 仅比较关心的属性
    lhs.someViewModel.lottieToShow == rhs.someViewModel.lottieToShow
  }
    
  var body: some View {
    Lottie(name: someViewModel.lottieToShow, loopMode: .playOnce)
      .frame(width: 250, height: 250)
  }
}

// 主视图中使用时添加`.equatable()`
struct ContentView: View {
  @StateObject var someViewModel: SomeViewModel

  var body: some View {
    LottieView(someViewModel: someViewModel)
      .equatable()
  }
}

此方法通过自定义相等性判断,告诉SwiftUI只有当lottieToShow变化时才需要更新视图。

3. 拆分ViewModel(按需使用)

如果ViewModel中的属性职责划分清晰,可以将其拆分为多个小的ObservableObject,让不同视图监听不同的子ViewModel,从根源上避免无关更新。

例如,拆分出专门管理动画的ViewModel:

class AnimationViewModel: ObservableObject {
  @Published var lottieToShow: String = "lottie_city_animation"
    
  private var isCity: Bool = false { didSet {
    updateLottie()
  }}
    
  private func updateLottie() {
    lottieToShow = isCity ? "lottie_city_animation" : "lottie_park_animation"
  }
    
  // 接收Firebase传来的isCity更新
  func updateIsCity(_ newValue: Bool) {
    guard isCity != newValue else { return }
    isCity = newValue
  }
}

class MainViewModel: ObservableObject {
  @Published var someOtherProperty: Bool = false
  let animationVM = AnimationViewModel()
    
  private func readObject() {
    ref.child("users").child("01")
      .observe(.value) { snapshot in
        do {
          let firebaseObject = try snapshot.data(as: Object.self)
          self.animationVM.updateIsCity(firebaseObject.isCity)
          if self.someOtherProperty != firebaseObject.someOtherProperty {
            self.someOtherProperty = firebaseObject.someOtherProperty
          }
        } catch {
          print("Error reading value")
        }
      }
  }
}

// 子视图仅监听AnimationViewModel
struct LottieView: View {
  @ObservedObject var animationVM: AnimationViewModel
    
  var body: some View {
    Lottie(name: animationVM.lottieToShow, loopMode: .playOnce)
      .frame(width: 250, height: 250)
  }
}

关键说明

  • 第一种方法是最直接的解决方案,通过减少视图依赖范围,从根本上避免不必要的重绘。
  • Equatable方法适合无法修改视图参数的场景,但需要手动维护相等性判断逻辑。
  • 拆分ViewModel适用于大型项目,能让职责更清晰,降低耦合度。

内容的提问来源于stack exchange,提问作者moosgrn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 05:10:22