如何避免SwiftUI状态更新时重绘所有子视图?
解决SwiftUI中状态更新时子视图批量重绘的问题
问题场景
你定义了可识别的Object类型、存储状态的ObjectStates结构体,在ContentView中通过List展示多个MyToggle子视图,绑定到objectStates的下标值。但切换任意一个Toggle时,所有MyToggle都会触发重绘;而添加额外的@State属性后,更新该属性时子视图却不会重绘。
原代码示例:
struct Object: Identifiable { let id = UUID() let name: String } struct ObjectStates { var states: [Object.ID: Bool] = [:] subscript(objectId: Object.ID) -> Bool { get { states[objectId, default: false] } set { states[objectId] = newValue } } } struct ContentView: View { let objects = [Object(name: "Toggle 1"), Object(name: "Toggle 2"), Object(name: "Toggle 3"), Object(name: "Toggle 4"), Object(name: "Toggle 5")] @State var objectStates = ObjectStates() var body: some View { List(objects) { object in MyToggle(name: object.name, isOn: $objectStates[object.id]) } } } struct MyToggle: View { let name: String @Binding var isOn: Bool var body: some View { let _ = print(name) let _ = Self._printChanges() Toggle(name, isOn: $isOn) } }
问题原因
ObjectStates是值类型结构体,当通过下标修改其中某个状态值时,整个objectStates实例会被标记为“已更改”。SwiftUI会认为父视图的状态发生了整体变化,进而重新计算父视图的body,并对所有子视图进行重绘检查——即使大部分子视图的绑定值并未实际改变。
而添加额外@State属性后,更新该属性时objectStates并未发生变化,SwiftUI能精准判断只有关联的主Toggle需要更新,因此子视图不会被触发重绘。
解决方案
有两种常用方式可以避免子视图批量重绘:
方案1:让子视图遵循Equatable,精准控制重绘时机
让MyToggle遵循Equatable协议,仅当isOn或name实际变化时才触发重绘:
struct MyToggle: View, Equatable { let name: String @Binding var isOn: Bool static func == (lhs: MyToggle, rhs: MyToggle) -> Bool { lhs.name == rhs.name && lhs.isOn == rhs.isOn } var body: some View { let _ = print(name) let _ = Self._printChanges() Toggle(name, isOn: $isOn) } }
然后在父视图的子视图声明后添加.equatable()修饰符:
List(objects) { object in MyToggle(name: object.name, isOn: $objectStates[object.id]) .equatable() }
方案2:将状态容器改为引用类型ObservableObject
把ObjectStates从结构体改为ObservableObject,利用SwiftUI的精准依赖追踪,仅当对应状态变化时通知子视图更新:
class ObjectStates: ObservableObject { @Published var states: [Object.ID: Bool] = [:] subscript(objectId: Object.ID) -> Bool { get { states[objectId, default: false] } set { states[objectId] = newValue } } }
在ContentView中使用@StateObject持有状态容器,并通过自定义Binding绑定到子视图:
struct ContentView: View { let objects = [Object(name: "Toggle 1"), Object(name: "Toggle 2"), Object(name: "Toggle 3"), Object(name: "Toggle 4"), Object(name: "Toggle 5")] @StateObject var objectStates = ObjectStates() var body: some View { List(objects) { object in MyToggle(name: object.name, isOn: Binding( get: { self.objectStates[object.id] }, set: { self.objectStates[object.id] = $0 } )) } } }
这样,修改某个状态时,只有绑定该状态的子视图会收到更新通知并触发重绘,其他子视图不受影响。
内容的提问来源于stack exchange,提问作者Ashley Mills
相关产品推荐
相关产品推荐

