Runt
2025-06-27 37251af47d1c5e622e8e62a76ff7077f9cd87069
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//
//  Untitled.swift
//  LiveProject
//
//  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()
    }
}