SwiftUI中如何为流式布局末尾添加样式统一的「添加」按钮?
问题描述
我已实现如图1所示的标签流式布局,但尝试在布局末尾添加样式统一的「添加」按钮时,无法正确对齐至末尾,期望效果如图2所示。以下是布局代码及自定义流式布局结构体,恳请协助解决。
布局代码
VStack ( //alignment: .leading ){ VStack { HStack { Text("✖️") Spacer() Button(action: { showCategoryPopUp.toggle() }) { Text("Done") .font(.system(size: 20)) .padding(10) .background(Color.yellow) .foregroundColor(.black) .cornerRadius(10) } } WrappingHStack(models: firstCategories, viewGenerator: { category in Group { Text(category.name) .font(.system(size: 12, weight: .black, design: .default)) .padding(.horizontal, 10) .padding(.vertical, 5) .frame(minWidth: 60) .background(selectedFirstCategoryID == category.id ? Color.red : Color.blue) .foregroundColor(selectedFirstCategoryID == category.id ? .white : .black) .cornerRadius(6) .contentShape(Rectangle()) .onTapGesture { selectedFirstCategoryID = category.id } .shake(animatableData: isShaking ? 1 : 0) .onLongPressGesture(minimumDuration: 0.5) { print("Long press detected for category: \(category.name)") isShaking.toggle() showDeleteIcon.toggle() } .simultaneousGesture(TapGesture().onEnded { if isShaking { isShaking = false showDeleteIcon = false } }) } }) HStack { Text("➕") .font(.system(size: 12, weight: .black, design: .default)) .padding(.vertical, 5) .frame(minWidth: 60) .background(Color.gray) .cornerRadius(6) .foregroundColor(.white) .contentShape(Rectangle()) .onTapGesture { print("Add new category") } Spacer() } } }
自定义流式布局结构体
struct WrappingHStack<Model, V>: View where Model: Hashable, V: View { typealias ViewGenerator = (Model) -> V var models: [Model] var viewGenerator: ViewGenerator var horizontalSpacing: CGFloat = 5 var verticalSpacing: CGFloat = 5 @State private var totalHeight = CGFloat.zero // << variant for ScrollView/List var body: some View { VStack { GeometryReader { geometry in self.generateContent(in: geometry) } } .frame(height: totalHeight)// << variant for ScrollView/List } private func generateContent(in geometry: GeometryProxy) -> some View { var width = CGFloat.zero var height = CGFloat.zero let addButton = Text("➕") .font(.system(size: 12, weight: .black, design: .default)) .padding(.horizontal, 10) .padding(.vertical, 5) .frame(minWidth: 60) .background(Color.gray) .cornerRadius(6) .foregroundColor(.white) .contentShape(Rectangle()) .onTapGesture { print("Add new category") } return ZStack(alignment: .topLeading) { ForEach(self.models, id: \.self) { model in viewGenerator(model) .padding(.horizontal, horizontalSpacing) .padding(.vertical, verticalSpacing) .alignmentGuide(.leading) { dimension in if (abs(width - dimension.width) > geometry.size.width) { width = 0 height -= dimension.height } let result = width if model == self.models.last! { width = 0 } else { width -= dimension.width } return result } .alignmentGuide(.top) { _ in let result = height if model == self.models.last! { height = 0 } return result } } addButton } .background(viewHeightReader($totalHeight)) } private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View { return GeometryReader { geometry -> Color in let rect = geometry.frame(in: .local) DispatchQueue.main.async { binding.wrappedValue = rect.size.height } return .clear } } }
解决方案
问题根源是当前「添加」按钮独立于流式布局的计算逻辑,没有参与换行判断,所以无法和标签自动对齐。下面提供两种实用解决方式:
方案一:将添加按钮作为特殊元素融入数据源
不需要修改自定义布局结构体,只需把添加按钮作为一个特殊的"虚拟分类"加入展示列表,让它参与流式布局计算:
- 给你的
Category模型添加标记(或用特殊ID区分):
struct Category: Hashable { let id: String let name: String let isAddButton: Bool // 标记是否为添加按钮 }
- 构造包含添加按钮的展示数据源:
var displayCategories: [Category] { var temp = firstCategories // 添加虚拟的添加按钮元素 temp.append(Category(id: "add_btn", name: "", isAddButton: true)) return temp }
- 在
WrappingHStack的视图生成器里判断并渲染对应视图:
WrappingHStack(models: displayCategories, viewGenerator: { category in if category.isAddButton { // 渲染添加按钮 Text("➕") .font(.system(size: 12, weight: .black, design: .default)) .padding(.horizontal, 10) .padding(.vertical, 5) .frame(minWidth: 60) .background(Color.gray) .cornerRadius(6) .foregroundColor(.white) .contentShape(Rectangle()) .onTapGesture { print("Add new category") } } else { // 原有的标签视图逻辑,和之前一致 Text(category.name) .font(.system(size: 12, weight: .black, design: .default)) .padding(.horizontal, 10) .padding(.vertical, 5) .frame(minWidth: 60) .background(selectedFirstCategoryID == category.id ? Color.red : Color.blue) .foregroundColor(selectedFirstCategoryID == category.id ? .white : .black) .cornerRadius(6) .contentShape(Rectangle()) .onTapGesture { selectedFirstCategoryID = category.id } .shake(animatableData: isShaking ? 1 : 0) .onLongPressGesture(minimumDuration: 0.5) { print("Long press detected for category: \(category.name)") isShaking.toggle() showDeleteIcon.toggle() } .simultaneousGesture(TapGesture().onEnded { if isShaking { isShaking = false showDeleteIcon = false } }) } })
方案二:修改自定义布局,支持尾部视图
如果不想改动数据源,可以扩展WrappingHStack,让它支持传入尾部自定义视图,自动参与流式布局计算:
- 修改
WrappingHStack结构体:
struct WrappingHStack<Model, V, Tail>: View where Model: Hashable, V: View, Tail: View { typealias ViewGenerator = (Model) -> V var models: [Model] var viewGenerator: ViewGenerator var horizontalSpacing: CGFloat = 5 var verticalSpacing: CGFloat = 5 var tailView: Tail? // 新增尾部视图参数 @State private var totalHeight = CGFloat.zero var body: some View { VStack { GeometryReader { geometry in generateContent(in: geometry) } } .frame(height: totalHeight) } private func generateContent(in geometry: GeometryProxy) -> some View { var width = CGFloat.zero var height = CGFloat.zero return ZStack(alignment: .topLeading) { // 遍历原有分类模型 ForEach(models, id: \.self) { model in viewGenerator(model) .padding(.horizontal, horizontalSpacing) .padding(.vertical, verticalSpacing) .alignmentGuide(.leading) { dimension in if (abs(width - dimension.width) > geometry.size.width) { width = 0 height -= dimension.height } let result = width // 如果有尾部视图,最后一个分类不重置width if model == models.last! && tailView == nil { width = 0 } else { width -= dimension.width } return result } .alignmentGuide(.top) { _ in let result = height if model == models.last! && tailView == nil { height = 0 } return result } } // 处理尾部视图的布局计算 if let tailView = tailView { tailView .padding(.horizontal, horizontalSpacing) .padding(.vertical, verticalSpacing) .alignmentGuide(.leading) { dimension in if (abs(width - dimension.width) > geometry.size.width) { width = 0 height -= dimension.height } let result = width width = 0 return result } .alignmentGuide(.top) { _ in let result = height height = 0 return result } } } .background(viewHeightReader($totalHeight)) } private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View { GeometryReader { geometry -> Color in let rect = geometry.frame(in: .local) DispatchQueue.main.async { binding.wrappedValue = rect.size.height } return .clear } } } // 扩展:支持无尾部视图的默认初始化 extension WrappingHStack where Tail == EmptyView { init(models: [Model], viewGenerator: @escaping ViewGenerator, horizontalSpacing: CGFloat = 5, verticalSpacing: CGFloat = 5) { self.models = models self.viewGenerator = viewGenerator self.horizontalSpacing = horizontalSpacing self.verticalSpacing = verticalSpacing self.tailView = nil } }
- 使用时直接传入尾部的添加按钮:
WrappingHStack(models: firstCategories, viewGenerator: { category in // 原有的标签视图逻辑,和之前一致 Text(category.name) .font(.system(size: 12, weight: .black, design: .default)) .padding(.horizontal, 10) .padding(.vertical, 5) .frame(minWidth: 60) .background(selectedFirstCategoryID == category.id ? Color.red : Color.blue) .foregroundColor(selectedFirstCategoryID == category.id ? .white : .black) .cornerRadius(6) .contentShape(Rectangle()) .onTapGesture { selectedFirstCategoryID = category.id } .shake(animatableData: isShaking ? 1 : 0) .onLongPressGesture(minimumDuration: 0.5) { print("Long press detected for category: \(category.name)") isShaking.toggle() showDeleteIcon.toggle() } .simultaneousGesture(TapGesture().onEnded { if isShaking { isShaking = false showDeleteIcon = false } }) }, tailView: { // 添加按钮视图 Text("➕") .font(.system(size: 12, weight: .black, design: .default)) .padding(.horizontal, 10) .padding(.vertical, 5) .frame(minWidth: 60) .background(Color.gray) .cornerRadius(6) .foregroundColor(.white) .contentShape(Rectangle()) .onTapGesture { print("Add new category") } })
两种方案都能让添加按钮自动融入流式布局,实现和标签对齐换行的效果,你可以根据自己的代码结构选择合适的方式。
内容的提问来源于stack exchange,提问作者cander
相关产品推荐
相关产品推荐

