ObservableObject多@Published属性引发不必要重绘的解决方法
解决ObservableObject引发的SwiftUI不必要视图更新问题
当使用ObservableObject中的@Published属性时,任意属性的变更都会触发持有该对象的父视图及其所有子视图的body重新计算,导致不必要的性能损耗,在iOS 14中甚至可能引发卡顿、冻结问题。以下是几种针对性的解决方案:
方案1:给子视图实现Equatable并启用相等性检查
SwiftUI默认会在父视图更新时重新渲染所有子视图,除非子视图实现Equatable协议,通过属性对比决定是否需要更新。
- 让子视图遵循
Equatable协议,仅对比关键依赖属性:
struct SomeSimpleComponent: View, Equatable { static func == (lhs: Self, rhs: Self) -> Bool { // 只有绑定的property值变化时,视图才需要更新 lhs.property == rhs.property } @Binding var property: Int var body: some View { print("component 1") HStack { Text("\(property)") Button("Increment") { property += 1 } } } } struct SomeSimpleComponent2: View, Equatable { static func == (lhs: Self, rhs: Self) -> Bool { lhs.property == rhs.property } @Binding var property: Int var body: some View { print("component 2") HStack { Text("\(property)") Button("Increment") { property += 1 } } } }
- 在父视图中调用子视图时添加
.equatable()修饰符:
struct ObjectBindingTest: View { @StateObject private var model = Model() var body: some View { print("——— top") VStack(spacing: 30) { SomeSimpleComponent(property: $model.property1).equatable() SomeSimpleComponent2(property: $model.property2).equatable() } .padding(50) } }
此时点击第一个按钮,控制台只会输出——— top和component 1,component 2不会触发body计算。
方案2:拆分ObservableObject为多个独立状态对象
将原有的大Model拆分为多个专注单个属性的小ObservableObject,让每个子视图仅观察自己依赖的状态对象,避免单个属性变更触发全局更新。
// 拆分后的独立状态对象 class Property1Model: ObservableObject { @Published var value: Int = 0 } class Property2Model: ObservableObject { @Published var value: Int = 0 } struct ObjectBindingTest: View { @StateObject private var model1 = Property1Model() @StateObject private var model2 = Property2Model() var body: some View { print("——— top") VStack(spacing: 30) { SomeSimpleComponent(model: model1) SomeSimpleComponent2(model: model2) } .padding(50) } } // 子视图仅观察自身需要的状态对象 struct SomeSimpleComponent: View { @ObservedObject var model: Property1Model var body: some View { print("component 1") HStack { Text("\(model.value)") Button("Increment") { model.value += 1 } } } } struct SomeSimpleComponent2: View { @ObservedObject var model: Property2Model var body: some View { print("component 2") HStack { Text("\(model.value)") Button("Increment") { model.value += 1 } } } }
更新model1的属性只会触发SomeSimpleComponent的更新,component 2因依赖的model2未变化,不会重新计算body。
方案3:使用Combine手动订阅特定属性(适用于只读/自定义更新逻辑)
如果子视图无需修改属性,或需要自定义更新时机,可以直接订阅目标属性的Publisher,避免观察整个ObservableObject。
class Model: ObservableObject { @Published var property1: Int = 0 @Published var property2: Int = 0 // 暴露单个属性的Publisher var property1Publisher: AnyPublisher<Int, Never> { $property1.eraseToAnyPublisher() } var property2Publisher: AnyPublisher<Int, Never> { $property2.eraseToAnyPublisher() } } struct ObjectBindingTest: View { @StateObject private var model = Model() var body: some View { print("——— top") VStack(spacing: 30) { SomeSimpleComponent( publisher: model.property1Publisher, initialValue: model.property1, onIncrement: { model.property1 += 1 } ) SomeSimpleComponent2( publisher: model.property2Publisher, initialValue: model.property2, onIncrement: { model.property2 += 1 } ) } .padding(50) } } struct SomeSimpleComponent: View { @State private var localValue: Int let publisher: AnyPublisher<Int, Never> let onIncrement: () -> Void init(publisher: AnyPublisher<Int, Never>, initialValue: Int, onIncrement: @escaping () -> Void) { self.publisher = publisher self._localValue = State(initialValue: initialValue) self.onIncrement = onIncrement } var body: some View { print("component 1") HStack { Text("\(localValue)") Button("Increment", action: onIncrement) } .onReceive(publisher) { newValue in localValue = newValue } } } // SomeSimpleComponent2实现逻辑与上述一致,此处省略
子视图仅在订阅的属性变更时更新,父视图更新不会影响子视图的body计算。
方案4:iOS 17+ 使用@Observable宏(推荐)
如果项目已升级到iOS 17或以上,苹果推出的@Observable宏可自动实现细粒度状态观察,无需手动拆分对象或实现Equatable:
import Observation @Observable class Model { var property1: Int = 0 var property2: Int = 0 } struct ObjectBindingTest: View { @State private var model = Model() var body: some View { print("——— top") VStack(spacing: 30) { SomeSimpleComponent(property: $model.property1) SomeSimpleComponent2(property: $model.property2) } .padding(50) } } // 子视图无需额外修改 struct SomeSimpleComponent: View { @Binding var property: Int var body: some View { print("component 1") HStack { Text("\(property)") Button("Increment") { property += 1 } } } }
@Observable会自动为每个属性生成独立订阅信号,只有依赖对应属性的视图才会更新,无需额外配置。
内容的提问来源于stack exchange,提问作者Morpheus
相关产品推荐
相关产品推荐

