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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 18:36:21