自适应网格布局 (AdaptiveGridLayout)

Swift Package Manager compatible

一个 SwiftUI 包,用于以宽度自适应的网格模式排列子视图,并在垂直方向上展开,例如用于一组标签 (Tags)。

起初,似乎内置的 LazyVStack 可以做到这一点,但尽管它可以创建自适应列,但该尺寸对于所有行都是固定的。

最低目标版本是 macOS 13iOS 16 (为了使用 Layout)。

1. 布局示例

import SwiftUI
import AdaptiveGridLayout

struct SampleView: View {

    let itemHeight: CGFloat = 50

    @State var items: [(width: CGFloat, color: Color)] = (0..<40).map { _ in
        (CGFloat.random(in: 10..<120),
         Color(hue: .random(in: 0..<1.0),
               saturation: .random(in: 0.1..<0.8),
               brightness:.random( in: 0.5..<0.9))
        )
    }

    var body: some View {
        AdaptiveVGrid(spacing: 2) { // <------------

            ForEach(items.indices, id: \.self) { index in
                Rectangle()
                    .fill(items[index].color)
                    .frame(width: items[index].width, height: itemHeight)
                    .overlay { Text(index, format: .number) }
            }

        }
        .border(.blue)
        .containerRelativeFrame(.vertical)
    }

}
Adaptive Layout Sample 1

2. 标签视图示例

import SwiftUI
import AdaptiveGridLayout

// MARK: View

struct TagsView: View {
    var model = FoodModel()
    var body: some View {
        AdaptiveVGrid(spacing: 6) {
            ForEach(model.fruits) { fruit in
                TagView(word: LocalizedStringKey(fruit.name))
            }
        }
    }
    
    struct TagView: View {
        let word: LocalizedStringKey
        var body: some View {
            Text(word)
                .font(.body.weight(.semibold).monospaced())
                .padding(.vertical, 4)
                .padding(.horizontal)
                .background(Color.red, in: Capsule(style: .circular))
                .foregroundStyle(.background)
        }
    }
}

// MARK: Model

class FoodModel {
    var fruits: [Fruit] = ["Tangerine", "Honeydew", "Fig", "Zucchini", "Orange", "Cherry", "Papaya", "Dragon Fruit", "Dates", "Lemon", "Apple", "Nectarine", "Raspberry", "Banana"]

    struct Fruit: Identifiable, ExpressibleByStringLiteral {
        let name: String
        var id: String { name }
        init(stringLiteral value: StringLiteralType) {
            name = value
        }
    }
}
Adaptive Layout Example 2

动画

元素自己处理动画。 为添加和删除元素添加一些动画的最简单方法是使用 matchedGeometry。 对于上面的示例,将 .matchedGeometryEffect(id: fruit.id, in: namespace) 添加到每个元素,然后添加 .animation(.spring, value: model.fruits)

looping_output