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

如何实现仅重绘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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 23:22:03