SwiftUI如何实现GridView内随内容自适应宽度的弹性视图
SwiftUI 实现内容自适应宽度的标签流布局
问题背景
- 实现需求:开发技能选择功能,需要将选中的技能以标签形式列表展示,每个标签尺寸由自身内容决定,超出容器宽度时自动换行
- 现有方案缺陷:使用
LazyVGrid实现时,无论配置.fixed()、.flexible()、.adaptive()哪种GridItem类型,最终都会生成等宽列的网格布局,无法实现单标签宽度随内容自适应的效果 - 目标效果参考:

- 运行环境:Xcode 13.3 / iOS 15.4
现有问题代码
let columns = [ GridItem(.adaptive(minimum: 100, maximum: .infinity)) ] VStack(alignment: .leading, spacing: 10) { Text("Interests") .font(.custom(Popins.bold.rawValue, size: 15)) LazyVGrid(columns: columns, spacing: 10) { ForEach(array, id: \.self) { item in SkillButton(title: item) { print("close") } } } } struct SkillButton: View { var title = "" var action: () -> Void var body: some View { Button { self.action() } label: { HStack { Text(title) .lineLimit(1) .font(.custom(Popins.medium.rawValue, size: 12)) .padding([.leading, .trailing], 10) .padding([.top, .bottom], 5) Image(systemName: "multiply") .resizable() .frame(width: 10, height: 10) .padding() } .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 5) .foregroundColor(UnifiedColor.appLighGary.opacity(0.2)) ) } } }
实现方案
LazyVGrid是为规则等宽网格场景设计的组件,本身不支持不等宽item自动换行的流式布局,不需要在GridItem参数上继续尝试,直接替换布局容器即可实现需求。
iOS 16+ 原生实现
iOS 16 新增的Layout协议可以非常方便地自定义布局,直接实现流式布局容器替换LazyVGrid即可,原有标签组件代码不需要任何修改:
struct FlowLayout: Layout { /// 标签之间的间距 var itemSpacing: CGFloat = 10 /// 行间距 var lineSpacing: CGFloat = 10 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let layoutResult = calculateLayout(in: proposal, subviews: subviews) return layoutResult.totalSize } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let layoutResult = calculateLayout(in: proposal, subviews: subviews) for (index, frame) in layoutResult.itemFrames.enumerated() { let adjustedPosition = CGPoint(x: frame.minX + bounds.minX, y: frame.minY + bounds.minY) subviews[index].place(at: adjustedPosition, proposal: ProposedViewSize(frame.size)) } } private func calculateLayout(in proposal: ProposedViewSize, subviews: Subviews) -> (totalSize: CGSize, itemFrames: [CGRect]) { let containerMaxWidth = proposal.width ?? .infinity var currentX: CGFloat = 0 var currentY: CGFloat = 0 var currentLineHeight: CGFloat = 0 var frames: [CGRect] = [] for subview in subviews { let itemSize = subview.sizeThatFits(.unspecified) // 当前行剩余空间不足,执行换行 if currentX + itemSize.width > containerMaxWidth, currentX > 0 { currentX = 0 currentY += currentLineHeight + lineSpacing currentLineHeight = 0 } frames.append(CGRect(x: currentX, y: currentY, width: itemSize.width, height: itemSize.height)) currentLineHeight = max(currentLineHeight, itemSize.height) currentX += itemSize.width + itemSpacing } let totalHeight = currentY + currentLineHeight return (CGSize(width: containerMaxWidth, height: totalHeight), frames) } }
调用方式如下:
VStack(alignment: .leading, spacing: 10) { Text("Interests") .font(.custom(Popins.bold.rawValue, size: 15)) // 替换原有LazyVGrid FlowLayout(itemSpacing: 10, lineSpacing: 10) { ForEach(array, id: \.self) { item in SkillButton(title: item) { print("close") } } } }
iOS 15 兼容实现
如果需要兼容更低版本,核心布局逻辑和上述FlowLayout一致,通过GeometryReader+PreferenceKey获取每个标签的尺寸,提前计算好所有标签的位置后再渲染即可,不需要依赖第三方库。
内容的提问来源于stack exchange,提问作者Qazi Ammar
相关产品推荐
相关产品推荐

