| | |
| | | // |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | import SwiftUI |
| | | import SwiftUI |
| | | |
| | | /// 完全自定义的自动换行布局容器 |
| | | struct FlowLayout<Data: RandomAccessCollection, Content: View>: View where Data.Element: Hashable { |
| | | // MARK: - 属性 |
| | | |
| | | /// 要显示的数据集合 |
| | | let data: Data |
| | | /// 水平间距 |
| | | let horizontalSpacing: CGFloat |
| | | /// 垂直间距 |
| | | let verticalSpacing: CGFloat |
| | | /// 对齐方式 |
| | | let alignment: HorizontalAlignment |
| | | /// 内容构建闭包 |
| | | let content: (Data.Element) -> Content |
| | | /// 总高度状态 |
| | | @State private var totalHeight: CGFloat = 0 |
| | | |
| | | // MARK: - 初始化 |
| | | |
| | | /// 初始化FlowLayout |
| | | /// - Parameters: |
| | | /// - data: 要显示的数据集合 |
| | | /// - horizontalSpacing: 水平间距,默认为8 |
| | | /// - verticalSpacing: 垂直间距,默认为8 |
| | | /// - alignment: 对齐方式,默认为.leading |
| | | /// - content: 内容构建闭包 |
| | | init( |
| | | _ data: Data, |
| | | horizontalSpacing: CGFloat = 8, |
| | | verticalSpacing: CGFloat = 8, |
| | | alignment: HorizontalAlignment = .leading, |
| | | @ViewBuilder content: @escaping (Data.Element) -> Content |
| | | ) { |
| | | self.data = data |
| | | self.horizontalSpacing = horizontalSpacing |
| | | self.verticalSpacing = verticalSpacing |
| | | self.alignment = alignment |
| | | self.content = content |
| | | } |
| | | |
| | | // MARK: - 主体视图 |
| | | |
| | | var body: some View { |
| | | GeometryReader { geometry in |
| | | self.contentView(in: geometry) |
| | | .background( |
| | | HeightReader(height: $totalHeight) |
| | | ) |
| | | } |
| | | .frame(height: totalHeight) |
| | | } |
| | | |
| | | // MARK: - 私有方法 |
| | | |
| | | /// 构建内容视图 |
| | | private func contentView(in geometry: GeometryProxy) -> some View { |
| | | var width: CGFloat = 0 |
| | | var height: CGFloat = 0 |
| | | var lastHeight: CGFloat = 0 |
| | | |
| | | return ZStack(alignment: Alignment(horizontal: alignment, vertical: .top)) { |
| | | ForEach(data.map { $0 }, id: \.self) { item in |
| | | content(item) |
| | | .padding(.trailing, horizontalSpacing) |
| | | .padding(.bottom, verticalSpacing) |
| | | .alignmentGuide(.leading) { dimensions in |
| | | // 检查是否需要换行 |
| | | if abs(width - dimensions.width) > geometry.size.width { |
| | | width = 0 |
| | | height += lastHeight + verticalSpacing |
| | | } |
| | | |
| | | let result = width |
| | | |
| | | // 更新宽度计算 |
| | | if item == data.last { |
| | | width = 0 // 重置为0,最后一项 |
| | | } else { |
| | | width -= dimensions.width + horizontalSpacing |
| | | } |
| | | |
| | | // 记录当前行高度 |
| | | lastHeight = dimensions.height |
| | | return result |
| | | } |
| | | .alignmentGuide(.top) { dimensions in |
| | | let result = height |
| | | |
| | | // 如果是最后一项,更新总高度 |
| | | if item == data.last { |
| | | height += lastHeight + verticalSpacing |
| | | } |
| | | |
| | | return result |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - 高度读取器 |
| | | |
| | | /// 用于读取视图高度的辅助视图 |
| | | private struct HeightReader: View { |
| | | @Binding var height: CGFloat |
| | | |
| | | var body: some View { |
| | | GeometryReader { geometry in |
| | | Color.clear |
| | | .preference( |
| | | key: HeightPreferenceKey.self, |
| | | value: geometry.size.height |
| | | ) |
| | | } |
| | | .onPreferenceChange(HeightPreferenceKey.self) { newHeight in |
| | | DispatchQueue.main.async { |
| | | self.height = newHeight |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - 高度偏好键 |
| | | |
| | | /// 用于传递高度值的PreferenceKey |
| | | private struct HeightPreferenceKey: PreferenceKey { |
| | | static var defaultValue: CGFloat = 0 |
| | | |
| | | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { |
| | | value = nextValue() |
| | | } |
| | | } |
| | | |
| | | // MARK: - 使用示例 |
| | | |
| | | struct FlowLayoutExample: View { |
| | | let tags = [ |
| | | "Swift", "SwiftUI", "UIKit", "Combine", "Core Data", |
| | | "Xcode", "Interface Builder", "Core Animation", "ARKit", |
| | | "Metal", "Core ML", "Vision", "MapKit", "CloudKit" |
| | | ] |
| | | |
| | | @State private var newTag = "" |
| | | @State private var customTags = ["自定义标签1", "自定义标签2"] |
| | | |
| | | var body: some View { |
| | | VStack { |
| | | FlowLayout(customTags + tags, horizontalSpacing: 10, verticalSpacing: 10) { tag in |
| | | MButton(text:tag){ |
| | | |
| | | } |
| | | } |
| | | .padding() |
| | | .animation(.default, value: customTags) |
| | | } |
| | | } |
| | | |
| | | private func addTag() { |
| | | withAnimation { |
| | | customTags.append(newTag) |
| | | newTag = "" |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// 标签视图 |
| | | struct TagView: View { |
| | | let text: String |
| | | |
| | | var body: some View { |
| | | Text(text) |
| | | .padding(.horizontal, 12) |
| | | .padding(.vertical, 8) |
| | | .background(Color.blue.opacity(0.2)) |
| | | .foregroundColor(.blue) |
| | | .cornerRadius(10) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 10) |
| | | .stroke(Color.blue, lineWidth: 1) |
| | | ) |
| | | .fixedSize(horizontal: true, vertical: false) // 确保文本不被截断 |
| | | } |
| | | } |
| | | |
| | | // MARK: - 预览 |
| | | |
| | | struct FlowLayout_Previews: PreviewProvider { |
| | | static var previews: some View { |
| | | FlowLayoutExample() |
| | | } |
| | | } |