SwiftUI:如何让 HStack 沿多行(如集合视图)包装子项? [英] SwiftUI: How to have HStack wrap children along multiple lines (like a collection view)?

查看:21
本文介绍了SwiftUI:如何让 HStack 沿多行(如集合视图)包装子项?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用 SwiftUI 重新创建基本的集合视图行为:

I'm trying to recreate basic collection view behavior with SwiftUI:

我有许多水平显示的视图(例如照片).当没有足够的空间在同一行显示所有照片时,剩余的照片应换行到下一行.

I have a number of views (e.g. photos) that are shown next to each other horizontally. When there is not enough space to show all photos on the same line, the remaining photos should wrap to the next line(s).

这是一个例子:

看起来可以使用一个 VStack 和多个 HStack 元素,每个元素包含一行的照片.

It looks like one could use one VStack with a number of HStack elements, each containing the photos for one row.

我尝试使用 GeometryReader 并迭代照片视图来动态创建这样的布局,但它不会编译(包含声明的闭包不能与函数构建器ViewBuilder"一起使用).是否可以动态创建视图并返回它们?

I tried using GeometryReader and iterating over the photo views to dynamically create such a layout, but it won't compile (Closure containing a declaration cannot be used with function builder 'ViewBuilder'). Is it possible to dynamically create views and return them?

说明:

盒子/照片可以有不同的宽度(与经典的网格"不同).棘手的部分是我必须知道当前框的宽度才能决定它是否适合当前行或是否必须开始新行.

The boxes/photos can be of different width (unlike a classical "grid"). The tricky part is that I have to know the width of the current box in order to decide if it fits on the current row of if I have to start a new row.

推荐答案

这是我使用 PreferenceKeys 解决这个问题的方法.

Here is how I solved this using PreferenceKeys.

public struct MultilineHStack: View {
    struct SizePreferenceKey: PreferenceKey {
        typealias Value = [CGSize]
        static var defaultValue: Value = []
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value.append(contentsOf: nextValue())
        }
    }

    private let items: [AnyView]
    @State private var sizes: [CGSize] = []

    public init<Data: RandomAccessCollection,  Content: View>(_ data: Data, @ViewBuilder content: (Data.Element) -> Content) {
          self.items = data.map { AnyView(content($0)) }
    }

    public var body: some View {
        GeometryReader {geometry in
            ZStack(alignment: .topLeading) {
                ForEach(0..<self.items.count) { index in
                    self.items[index].background(self.backgroundView()).offset(self.getOffset(at: index, geometry: geometry))
                }
            }
        }.onPreferenceChange(SizePreferenceKey.self) {
                self.sizes = $0
        }
    }

    private func getOffset(at index: Int, geometry: GeometryProxy) -> CGSize {
        guard index < sizes.endIndex else {return .zero}
        let frame = sizes[index]
        var (x,y,maxHeight) = sizes[..<index].reduce((CGFloat.zero,CGFloat.zero,CGFloat.zero)) {
            var (x,y,maxHeight) = $0
            x += $1.width
            if x > geometry.size.width {
                x = $1.width
                y += maxHeight
                maxHeight = 0
            }
            maxHeight = max(maxHeight, $1.height)
            return (x,y,maxHeight)
        }
        if x + frame.width > geometry.size.width {
            x = 0
            y += maxHeight
        }
        return .init(width: x, height: y)
    }

    private func backgroundView() -> some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(
                    key: SizePreferenceKey.self,
                    value: [geometry.frame(in: CoordinateSpace.global).size]
                )
        }
    }
}

你可以这样使用它:

struct ContentView: View {
    let texts = ["a","lot","of","texts"]
    var body: some View {
        MultilineHStack(self.texts) {
            Text($0)
        }
    }
}

它不仅适用于 Text,还适用于任何视图.

It works not only with Text, but with any views.

这篇关于SwiftUI:如何让 HStack 沿多行(如集合视图)包装子项?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆