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

