//
|
// 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()
|
}
|
}
|