1
Runt
2025-06-27 bf9e4680eb466bcb7c9cb1bef567252bb1f2bb7d
LiveProject/views/FlowLayout.swift
@@ -4,4 +4,197 @@
//
//  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()
    }
}