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
相关产品推荐
相关产品推荐

