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

SwiftUI如何按最大宽度限制HStack视图数量实现标签截断展示

实现思路
  • 通过GeometryReader获取父容器的可用最大宽度,提前预留截断指示器的显示空间
  • 渲染隐藏的全量标签,通过PreferenceKey采集每个标签的实际渲染宽度
  • 从左到右累加标签宽度(含标签间距),直到超出可用宽度为止,得到可显示的标签列表
  • 按leading对齐渲染可见标签,未全量显示时末尾追加截断指示器
完整实现代码

首先是用于测量标签宽度的PreferenceKey:

struct TagWidthPreferenceKey: PreferenceKey {
    static var defaultValue: [CGFloat] = []
    static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
        value.append(contentsOf: nextValue())
    }
}

然后是标签模型和自定义布局组件:

struct Tag: Identifiable {
    let id = UUID()
    let text: String
}

struct TruncatedTagHStack: View {
    let tags: [Tag]
    /// 标签之间的间距
    private let tagSpacing: CGFloat = 8
    /// 截断指示器预留宽度,可根据指示器样式调整
    private let indicatorReservedWidth: CGFloat = 32
    /// 存储所有标签的实际测量宽度
    @State private var tagWidths: [CGFloat] = []
    /// 父容器可用总宽度
    @State private var totalAvailableWidth: CGFloat = 0
    
    var body: some View {
        GeometryReader { proxy in
            HStack(spacing: tagSpacing) {
                // 隐藏的全量标签,仅用于测量宽度
                ForEach(tags) { tag in
                    tagText(tag.text)
                        .hidden()
                        .background(
                            GeometryReader { tagProxy in
                                Color.clear
                                    .preference(key: TagWidthPreferenceKey.self, value: [tagProxy.size.width])
                            }
                        )
                }
                
                // 实际渲染的可见标签
                ForEach(visibleTags) { tag in
                    tagText(tag.text)
                }
                
                // 截断指示器
                if !showAllTags {
                    Text("+\(tags.count - visibleTags.count)")
                        .padding(.horizontal, 6)
                        .padding(.vertical, 4)
                        .background(Color.blue.opacity(0.2))
                        .foregroundColor(.blue)
                        .cornerRadius(4)
                }
            }
            .onPreferenceChange(TagWidthPreferenceKey.self) { widths in
                tagWidths = widths
            }
            .onAppear {
                // 总可用宽度减去指示器预留宽度
                totalAvailableWidth = proxy.size.width - indicatorReservedWidth
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
        .frame(height: 30) // 固定高度,可根据标签样式调整
    }
    
    // 复用标签样式
    private func tagText(_ text: String) -> some View {
        Text(text)
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .background(Color.gray.opacity(0.2))
            .cornerRadius(4)
            .fixedSize()
    }
    
    // 计算可见标签列表
    private var visibleTags: [Tag] {
        guard !tagWidths.isEmpty, tagWidths.count == tags.count else { return [] }
        var currentTotalWidth: CGFloat = 0
        var result: [Tag] = []
        for (index, width) in tagWidths.enumerated() {
            let addWidth = width + (index == 0 ? 0 : tagSpacing)
            if currentTotalWidth + addWidth <= totalAvailableWidth {
                currentTotalWidth += addWidth
                result.append(tags[index])
            } else {
                break
            }
        }
        return result
    }
    
    // 判断是否已显示全部标签
    private var showAllTags: Bool {
        visibleTags.count == tags.count
    }
}
使用示例
struct ContentView: View {
    let sampleTags = [
        Tag(text: "SwiftUI"), Tag(text: "iOS开发"), Tag(text: "布局适配"),
        Tag(text: "单行标签"), Tag(text: "截断显示"), Tag(text: "自适应宽度"),
        Tag(text: "HStack")
    ]
    
    var body: some View {
        // 可放在任意父容器内,自动适配父容器宽度
        List {
            TruncatedTagHStack(tags: sampleTags)
                .listRowInsets(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16))
        }
    }
}
适配说明
  • 标签样式、间距、截断指示器样式可根据业务需求自行修改,对应调整预留的指示器宽度和组件固定高度即可
  • 若需要支持标签列表动态更新,可给tags添加@Binding修饰符,同时新增.onChange(of: tags)修饰符重置tagWidths触发重新计算
  • 无需手动指定宽度,自动适配父容器(包括全屏、List单元格、自定义卡片等任意场景)的可用宽度

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 22:18:01