如何实现仅重绘LazyHStack中选中的条目
SwiftUI LazyHStack 点击条目避免全局重绘的优化方案
我在使用SwiftUI的LazyHStack展示条目时,发现每次点击条目切换选中状态(绘制边框),整个HStack和所有条目都会被重绘,通过自定义的rainbowDebug可以直观看到所有视图都在重新渲染。以下是最小复现代码:
import SwiftUI private let rainbowDebugColors = [Color.purple, Color.blue, Color.green, Color.yellow, Color.orange, Color.red] extension View { func rainbowDebug() -> some View { self.background(rainbowDebugColors.randomElement()!) } } enum MyEnum: String, CaseIterable { case first = "first" case second = "second" case third = "third" case fourth = "fourth" case fifth = "fifth" } struct MyModel: Identifiable { let id: String let title: String let type: MyEnum init(type: MyEnum) { self.id = type.rawValue self.type = type self.title = type.rawValue.uppercased() } } final class MyViewModel: ObservableObject { @Published var selection: MyEnum? let types: [MyModel] = MyEnum.allCases.map { MyModel(type: $0) } } struct MyItemView: View { let model: MyModel @Binding var isSelected: Bool var body: some View { ZStack { RoundedRectangle(cornerRadius: 20) .fill(Color.white) .frame(width: 100, height: 130) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(isSelected ? .purple : .clear, lineWidth: 3) ) VStack { Text(model.title) .font(.headline) .foregroundColor(.black) .rainbowDebug() } }.onTapGesture { isSelected.toggle() } } } struct MySelectorView: View { @StateObject private var viewModel = MyViewModel() var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 20) { ForEach(viewModel.types, id: \.type) { model in MyItemView ( model: model, isSelected: Binding(get: { viewModel.selection == model.type }, set: { value in viewModel.selection = model.type }) ) } } .frame(height: 120) .padding() }.rainbowDebug() } } struct ContentView: View { var body: some View { MySelectorView() } }
要实现仅让选中/取消选中的条目重绘,核心是让每个MyItemView只响应自身相关的状态变化,而非全局selection变量更新触发全量刷新。可以通过以下几步优化:
1. 让MyItemView遵循Equatable
让视图仅在依赖属性变化时重绘,SwiftUI会自动对比新旧值,跳过无变化的视图渲染:
struct MyItemView: View, Equatable { static func == (lhs: MyItemView, rhs: MyItemView) -> Bool { // 仅当条目类型或选中状态变化时,才触发重绘 lhs.model.type == rhs.model.type && lhs.isSelected == rhs.isSelected } let model: MyModel let isSelected: Bool let onTap: () -> Void var body: some View { ZStack { RoundedRectangle(cornerRadius: 20) .fill(Color.white) .frame(width: 100, height: 130) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(isSelected ? .purple : .clear, lineWidth: 3) ) VStack { Text(model.title) .font(.headline) .foregroundColor(.black) .rainbowDebug() } } .onTapGesture(perform: onTap) } }
这里的关键改动:
- 移除
@Binding,改用isSelected常量和onTap闭包传递事件,避免每个视图持有全局viewModel的绑定引用 - 实现
Equatable协议,精准控制重绘触发条件
2. 优化父视图的条目渲染逻辑
在MySelectorView中,用EquatableView包裹MyItemView(或依赖视图自身的Equatable实现),调整选中状态传递方式:
struct MySelectorView: View { @StateObject private var viewModel = MyViewModel() var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 20) { ForEach(viewModel.types, id: \.type) { model in EquatableView(content: MyItemView( model: model, isSelected: viewModel.selection == model.type, onTap: { // 点击时切换选中状态:已选中则取消,未选中则设置为当前条目 viewModel.selection = viewModel.selection == model.type ? nil : model.type } )) } } .frame(height: 120) .padding() } .rainbowDebug() } }
3. 进阶优化:拆分静态与动态视图层级
如果条目视图更复杂,可将静态内容(如标题、背景)和动态内容(选中边框)拆分为独立子视图,静态部分不会因选中状态变化重绘:
// 静态内容视图,仅渲染一次 struct MyItemStaticView: View { let model: MyModel var body: some View { ZStack { RoundedRectangle(cornerRadius: 20) .fill(Color.white) .frame(width: 100, height: 130) VStack { Text(model.title) .font(.headline) .foregroundColor(.black) .rainbowDebug() } } } } // 动态控制的主视图 struct MyItemView: View, Equatable { static func == (lhs: MyItemView, rhs: MyItemView) -> Bool { lhs.model.type == rhs.model.type && lhs.isSelected == rhs.isSelected } let model: MyModel let isSelected: Bool let onTap: () -> Void var body: some View { ZStack { MyItemStaticView(model: model) // 仅选中时渲染边框 if isSelected { RoundedRectangle(cornerRadius: 20) .stroke(.purple, lineWidth: 3) .frame(width: 100, height: 130) } } .onTapGesture(perform: onTap) } }
效果验证
修改后,点击条目时只有被点击的条目会触发rainbowDebug颜色变化,其他条目和整个LazyHStack均不会重绘,大幅提升了渲染效率,尤其适用于条目数量较多的场景。
内容的提问来源于stack exchange,提问作者Whirlwind
相关产品推荐
相关产品推荐

