SwiftUI中如何实现LazyVGrid的区块对齐效果?
SwiftUI实现区块对齐的自定义网格布局
为什么原生LazyVGrid达不到预期?
LazyVGrid是行驱动的网格布局,每行高度由该行最高元素决定,所有元素都会被“拉平”到同一行高,没法实现不同尺寸元素的流式填充(也就是你要的区块对齐/瀑布流效果)。要实现需求,得自己写自定义布局。
解决方案:用Layout协议实现自适应布局(iOS 16+)
我们可以基于SwiftUI的Layout协议,实现一个能自动把元素放到当前最矮列的布局,完美适配大(b)、小(s)元素的动态组合。
1. 先定义元素尺寸规则
先明确大小元素的尺寸,这里假设小元素是80x80,大元素是80x160(你可以根据需求随便改):
enum GridBlockSize { case small // s case big // b var size: CGSize { switch self { case .small: return CGSize(width: 80, height: 80) case .big: return CGSize(width: 80, height: 160) } } }
2. 实现自定义瀑布流布局
这个布局会跟踪每一列的当前高度,每次把新元素放到高度最低的列里,自动填充空隙:
struct BlockAlignedGridLayout: Layout { let columnCount: Int let spacing: CGFloat func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { guard !subviews.isEmpty else { return .zero } // 计算每列的基础宽度 let totalSpacing = CGFloat(columnCount - 1) * spacing let columnWidth = (proposal.width ?? 0) - totalSpacing let itemWidth = columnWidth / CGFloat(columnCount) // 记录每列的总高度 var columnHeights = Array(repeating: 0.0, count: columnCount) for subview in subviews { // 找到当前最矮的列 let shortestColumnIndex = columnHeights.firstIndex(of: columnHeights.min()!)! // 获取元素的实际高度 let itemSize = subview.sizeThatFits(.init(width: itemWidth, height: nil)) // 更新列高度 columnHeights[shortestColumnIndex] += itemSize.height + spacing } return CGSize( width: proposal.width ?? 0, height: columnHeights.max()! - spacing // 减去最后一个多余的间距 ) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { guard !subviews.isEmpty else { return } let totalSpacing = CGFloat(columnCount - 1) * spacing let columnWidth = bounds.width - totalSpacing let itemWidth = columnWidth / CGFloat(columnCount) var columnHeights = Array(repeating: bounds.minY, count: columnCount) for subview in subviews { let shortestColumnIndex = columnHeights.firstIndex(of: columnHeights.min()!)! // 计算元素的位置 let x = bounds.minX + CGFloat(shortestColumnIndex) * (itemWidth + spacing) let y = columnHeights[shortestColumnIndex] // 获取元素尺寸 let itemSize = subview.sizeThatFits(.init(width: itemWidth, height: nil)) // 放置元素 subview.place( at: CGPoint(x: x, y: y), proposal: .init(width: itemWidth, height: itemSize.height) ) // 更新列高度 columnHeights[shortestColumnIndex] += itemSize.height + spacing } } }
3. 用自定义布局构建动态网格
根据你的元素组合(比如[.big, .small, .big])生成视图:
struct DynamicBlockGrid: View { // 这里可以替换成你的动态数据源,比如从接口获取的组合 let blockSequence: [GridBlockSize] = [.big, .small, .big, .small, .small, .big, .small] var body: some View { ScrollView { BlockAlignedGridLayout(columnCount: 2, spacing: 16) { ForEach(blockSequence.indices, id: \.self) { idx in let block = blockSequence[idx] ZStack { RoundedRectangle(cornerRadius: 8) .fill(Color.accentColor.opacity(0.8)) Text(block == .small ? "s" : "b") .font(.title) .foregroundColor(.white) } .frame(width: block.size.width, height: block.size.height) } } .padding() } } }
4. 适配大元素占多列的情况
如果你的大元素是占2列(比如宽度是小元素的2倍),可以修改布局逻辑:判断元素类型后,找到连续可用的多列,放置大元素后同时更新这些列的高度,核心逻辑可以基于上面的Layout扩展实现。
备选方案
如果不想自己写布局,也可以用第三方库,比如SwiftUIMasonry,直接集成就能实现类似效果。
内容的提问来源于stack exchange,提问作者Sjakelien
相关产品推荐
相关产品推荐

