Runt
2025-07-27 7b3ecfffc59d2d980d9f7628365b64c20fe015be
LiveProject/views/FlowLayout.swift
@@ -5,163 +5,89 @@
//  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 {
struct FlowLayout: Layout {
    var spacing: CGFloat = 8
    var lineSpacing: CGFloat = 8
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        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
                    }
        var currentLineWidth: CGFloat = 0
        var currentLineHeight: CGFloat = 0
        let maxWidth = proposal.width ?? .infinity
        for view in subviews {
            let size = view.sizeThatFits(.unspecified)
            if currentLineWidth + size.width > maxWidth {
                width = max(width, currentLineWidth)
                height += currentLineHeight + lineSpacing
                currentLineWidth = size.width
                currentLineHeight = size.height
            } else {
                currentLineWidth += size.width + spacing
                currentLineHeight = max(currentLineHeight, size.height)
            }
        }
        width = max(width, currentLineWidth)
        height += currentLineHeight
        return CGSize(width: width, height: height)
    }
}
// MARK: - 高度读取器
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var x: CGFloat = 0
        var y: CGFloat = 0
        var lineHeight: CGFloat = 0
/// 用于读取视图高度的辅助视图
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
        for view in subviews {
            let size = view.sizeThatFits(.unspecified)
            if x + size.width > bounds.width {
                x = 0
                y += lineHeight + lineSpacing
                lineHeight = 0
            }
            view.place(
                at: CGPoint(x: bounds.minX + x, y: bounds.minY + y),
                proposal: ProposedViewSize(width: size.width, height: size.height)
            )
            x += size.width + spacing
            lineHeight = max(lineHeight, size.height)
        }
    }
}
// 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"
        "Swift"
    ]
    
    @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){
        VStack{
            VStack(spacing: 20) {
                FlowLayout(){
                    
                }
                        ForEach(tags, id: \.self) { item in
                            Text(item)
                                .padding(.horizontal, 12)
                                .padding(.vertical, 6)
                                .background(Color.blue.opacity(0.2))
                                .cornerRadius(8)
                        }
                }.frame(alignment:.leading)
                .background(Color.red)
            }
            .padding()
            .animation(.default, value: customTags)
            .frame(maxWidth: .infinity,alignment:.leading)
        }
        .background(Color.black)
    }
    
    private func addTag() {