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

SwiftUI动态调整LazyHGrid行高:随可见项自适应高度

解决LazyHGrid背景高度自适应可见项的优化方案

针对你遇到的LazyHGrid始终以第一个元素高度计算容器高度、背景无法自适应当前可见项高度,且现有方案卡顿复杂的问题,下面提供两种高效的解决思路:

方案1:基于滚动追踪+PreferenceKey的动态高度适配

这个方案通过追踪可见项的高度,动态更新背景容器的高度,逻辑清晰且性能可控:

实现步骤

  1. 用PreferenceKey收集每个网格项的实际高度;
  2. 通过onAppear/onDisappear标记当前可见的项;
  3. 滚动时同步计算可见项的最大高度,更新容器高度。

完整代码

import SwiftUI

// 用于收集视图高度的PreferenceKey
struct ViewHeightKey: PreferenceKey {
    static var defaultValue: [UUID: CGFloat] = [:]
    static func reduce(value: inout [UUID: CGFloat], nextValue: () -> [UUID: CGFloat]) {
        value.merge(nextValue()) { $1 }
    }
}

struct AdaptiveHeightLazyHGrid: View {
    // 模拟动态内容数据
    let items: [(id: UUID, text: String)] = [
        (UUID(), "短文本"),
        (UUID(), "较长的文本,会换行显示,导致高度增加"),
        (UUID(), "非常长的文本内容,需要占据多行空间,测试高度自适应的效果"),
        (UUID(), "普通长度文本"),
        (UUID(), "超长长长长长长长长长长长长长长长长长长长长长长长长长长长文本")
    ]
    
    let columns = [GridItem(.flexible())]
    
    @State private var itemHeights: [UUID: CGFloat] = [:]
    @State private var containerHeight: CGFloat = 0
    @State private var visibleItemIDs: Set<UUID> = []
    
    var body: some View {
        VStack {
            // 自适应高度的浅红色背景
            Color.red.opacity(0.2)
                .frame(height: containerHeight)
                .overlay(
                    ScrollView(.horizontal, showsIndicators: false) {
                        ScrollViewReader { proxy in
                            LazyHGrid(rows: columns, spacing: 16) {
                                ForEach(items, id: \.id) { item in
                                    Text(item.text)
                                        .padding()
                                        .background(Color.white)
                                        .cornerRadius(8)
                                        .id(item.id)
                                        // 收集当前项的高度
                                        .background(
                                            GeometryReader { geo in
                                                Color.clear
                                                    .preference(key: ViewHeightKey.self, value: [item.id: geo.size.height])
                                            }
                                        )
                                        // 更新可见项集合
                                        .onAppear {
                                            visibleItemIDs.insert(item.id)
                                            updateContainerHeight()
                                        }
                                        .onDisappear {
                                            visibleItemIDs.remove(item.id)
                                            updateContainerHeight()
                                        }
                                }
                            }
                            .padding()
                            // 滚动结束后同步更新高度,避免频繁计算
                            .onChange(of: proxy.scrollViewContentOffset) { _ in
                                DispatchQueue.main.async {
                                    updateContainerHeight()
                                }
                            }
                        }
                    }
                )
            
            // 垂直布局的其他内容
            Text("下方的垂直布局内容")
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.blue.opacity(0.2))
        }
        .onPreferenceChange(ViewHeightKey.self) { newHeights in
            itemHeights.merge(newHeights) { $1 }
            updateContainerHeight()
        }
    }
    
    // 计算当前可见项的最大高度
    private func updateContainerHeight() {
        let visibleHeights = visibleItemIDs.compactMap { itemHeights[$0] }
        containerHeight = visibleHeights.max() ?? 0
    }
}

struct AdaptiveHeightLazyHGrid_Previews: PreviewProvider {
    static var previews: some View {
        AdaptiveHeightLazyHGrid()
    }
}

方案2:自定义Layout实现高性能适配

如果方案1仍存在卡顿,推荐使用SwiftUI的自定义Layout协议,直接在布局阶段处理高度计算,更贴近系统布局逻辑,性能更优:

核心思路

自定义横向布局,在sizeThatFits中计算可见项的最大高度作为布局整体高度,避免频繁的状态更新触发重绘。

完整代码

import SwiftUI

struct HorizontalAdaptiveLayout: Layout {
    var spacing: CGFloat = 16
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard !subviews.isEmpty else { return .zero }
        
        let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let totalWidth = subviewSizes.reduce(0) { $0 + $1.width } + spacing * CGFloat(subviews.count - 1)
        // 可结合滚动偏移优化,仅计算可见区域内的子视图高度
        let maxHeight = subviewSizes.map { $0.height }.max() ?? 0
        
        return CGSize(width: totalWidth, height: maxHeight)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var x = bounds.minX
        for subview in subviews {
            let size = subview.sizeThatFits(.unspecified)
            subview.place(at: CGPoint(x: x, y: bounds.minY), proposal: ProposedViewSize(size))
            x += size.width + spacing
        }
    }
}

struct CustomLayoutDemo: View {
    let items: [(id: UUID, text: String)] = [
        (UUID(), "短文本"),
        (UUID(), "较长的文本,会换行显示,导致高度增加"),
        (UUID(), "非常长的文本内容,需要占据多行空间,测试高度自适应的效果"),
        (UUID(), "普通长度文本"),
        (UUID(), "超长长长长长长长长长长长长长长长长长长长长长长长长长长长文本")
    ]
    
    @State private var containerHeight: CGFloat = 0
    
    var body: some View {
        VStack {
            Color.red.opacity(0.2)
                .frame(height: containerHeight)
                .overlay(
                    ScrollView(.horizontal, showsIndicators: false) {
                        HorizontalAdaptiveLayout(spacing: 16) {
                            ForEach(items, id: \.id) { item in
                                Text(item.text)
                                    .padding()
                                    .background(Color.white)
                                    .cornerRadius(8)
                                    .background(
                                        GeometryReader { geo in
                                            Color.clear
                                                .onAppear {
                                                    containerHeight = max(containerHeight, geo.size.height)
                                                }
                                        }
                                    )
                            }
                        }
                        .padding()
                        // 滚动时更新可见项最大高度(可结合ScrollViewReader精准优化)
                        .background(
                            GeometryReader { geo in
                                Color.clear
                                    .onChange(of: geo.frame(in: .global).origin.x) { _ in
                                        DispatchQueue.main.async {
                                            // 此处可优化为仅计算可见区域内的视图高度
                                            containerHeight = items.compactMap { _ in
                                                // 实际需结合滚动位置判断可见性,此处简化示例
                                                nil
                                            }.max() ?? 0
                                        }
                                    }
                            }
                        )
                    }
                )
            
            Text("下方的垂直布局内容")
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.blue.opacity(0.2))
        }
    }
}

struct CustomLayoutDemo_Previews: PreviewProvider {
    static var previews: some View {
        CustomLayoutDemo()
    }
}

关键优化点

  • 减少不必要的计算:仅追踪当前可见项的高度,避免遍历所有子视图;
  • 延迟更新:用DispatchQueue.main.async在滚动结束后再更新高度,避免滚动过程中频繁重绘;
  • 精准可见性判断:结合ScrollViewReader的visibleRegion或scrollViewContentOffset,准确识别当前可见的项,进一步降低计算量。

内容的提问来源于stack exchange,提问作者Robert Dresler

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 07:55:20