0

我需要在水平视图中显示标签。标签应该只显示一行,但如果标签的数量超出了给定的宽度,它应该尽可能多地显示,并在末尾加上一个截断指示符。当有足够的空间可以容纳所有标签时,它们应该是前导对齐的。

截断的 HStack

最大宽度定义为其包含视图的宽度。作为一个实际示例,这可能是设备的整个宽度,或者是列表的宽度

如何在 SwiftUI 中实现这一点?

我从 HStack 开始,但我找不到任何方法来限制基于宽度的视图数量......

我尝试调整这个问题中关于将项目包装在 HStack 中的答案(这是一个类似的问题,但与我的不完全相同)。我无法到达我需要的地方。包装工作,但它似乎没有将其结果高度传达给父视图,导致包含视图中的重叠和布局问题......

4

1 回答 1

0

非常感谢上面评论中的@CouchDeveloper。

将他们的示例用于包装 HStack,我能够对其进行修改以执行我需要的操作。作为奖励,我添加了对maxRows允许您控制包装限制为多少行的参数的支持。对于我的情况,我将其设置为 1,如果有空间,则添加截断指示符,否则删除行中的最后一项,然后添加截断指示符。

这是它在行动

代码如下。它主要是 @CouchDeveloper 的代码,并添加了我的修改。

import SwiftUI

struct WrappingHStack<Content: View, T: Hashable>: View {
    private typealias Row = [T]
    private typealias Rows = [Row]

    private struct Layout: Equatable {
        let cellAlignment: VerticalAlignment
        let cellSpacing: CGFloat
        let width: CGFloat
        let maxRows: Int?
    }

    private let data: [T]
    private let truncatedItem: T?
    private let content: (T) -> Content
    private let layout: Layout

    @State private var rows: Rows = Rows()
    @State private var sizes: [CGSize] = [CGSize]()

    /// Initialises a WrappingHStack instance.
    /// - Parameters:
    ///   - data: An array of elements of type `T` whose elements are used to initialise a "cell" view.
    ///   - truncatedItem: An item used to indicate truncation when the max number of rows has been displayed but there are other items not displayed.
    ///   - cellAlignment: An alignment position along the horizontal axis.
    ///   - cellSpacing: The spacing between the cell views.
    ///   - width: The width of the container view.
    ///   - maxRows: The maximum number of rows that will be displayed regardless of how many items there are.
    ///   - content: Returns a cell view.
    init(
        data: [T],
        truncatedItem: T? = nil,
        cellAlignment: VerticalAlignment = .firstTextBaseline,
        cellSpacing: CGFloat = 8,
        width: CGFloat,
        maxRows: Int? = nil,
        content: @escaping (T) -> Content
    ) {
        self.data = data
        self.truncatedItem = truncatedItem
        self.content = content
        self.layout = .init(
            cellAlignment: cellAlignment,
            cellSpacing: cellSpacing,
            width: width,
            maxRows: maxRows
        )
    }

    var body: some View {
        buildView(
            rows: rows,
            content: content,
            layout: layout
        )
    }

    @ViewBuilder
    private func buildView(rows: Rows, content: @escaping (T) -> Content, layout: Layout) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            ForEach(rows, id: \.self) { row in
                HStack(alignment: layout.cellAlignment, spacing: layout.cellSpacing) {
                    ForEach(row, id: \.self) { value in
                        content(value)
                    }
                }
            }
        }
        .background(
            calculateCellSizesAndRows(data: data, content: content) { sizes in
                self.sizes = sizes
            }
            .onChange(of: layout) { layout in
                self.rows = calculateRows(layout: layout)
            }
        )
    }

    // Populates a HStack with the calculated cell content. The size of each cell
    // will be stored through a view preference accessible with key
    // `SizeStorePreferenceKey`. Once the cells are layout, the completion
    // callback `result` will be called with an array of CGSize
    // representing the cell sizes as its argument. This should be used to store
    // the size array in some state variable. The function continues to calculate
    // the rows based on the cell sizes and the layout.
    // Returns the hidden HStack. This HStack will never be rendered on screen.
    // Will be called only when data or content changes. This is likely the
    // most expensive part, since it requires calculating the size of each
    // cell.
    private func calculateCellSizesAndRows(
        data: [T],
        content: @escaping (T) -> Content,
        result: @escaping ([CGSize]) -> Void
    ) -> some View {
        // Note: the HStack is required to layout the cells as _siblings_ which
        // is required for the SizeStorePreferenceKey's reduce function to be
        // invoked.
        HStack {
            ForEach(data, id: \.self) { element in
                content(element)
                    .calculateSize()
            }
        }
        .onPreferenceChange(SizeStorePreferenceKey.self) { sizes in
            result(sizes)
            self.rows = calculateRows(layout: layout)
        }
        .hidden()
    }

    // Will be called when the layout changes. This happens whenever the
    // orientation of the device changes or when the content views changes
    // its size. This function is quite inexpensive, since the cell sizes will
    // not be calclulated.
    private func calculateRows(layout: Layout) -> Rows {
        guard layout.width > 10 else {
            return []
        }
        let dataAndSize = zip(data, sizes)
        var rows = [[T]]()
        var availableSpace = layout.width
        var elements = ArraySlice(dataAndSize)
        while let (data, size) = elements.first {
            var row = [data]
            availableSpace -= size.width + layout.cellSpacing
            elements = elements.dropFirst()
            while let (nextData, nextSize) = elements.first, (nextSize.width + layout.cellSpacing) <= availableSpace {
                row.append(nextData)
                availableSpace -= nextSize.width + layout.cellSpacing
                elements = elements.dropFirst()
            }
            rows.append(row)
            if
                let maxRows = layout.maxRows,
                maxRows > 0,
                rows.count >= maxRows,
                !elements.isEmpty
            {
                if let truncatedItem = truncatedItem {
                    if availableSpace < 20 { // This hardcoded value is good enough for now, but will need to be calculated like the other cell sizes if a differently-sized truncation item is used.
                        row = row.dropLast()
                    }
                    row.append(truncatedItem)
                }
                rows = rows.dropLast()
                rows.append(row)
                break
            }
            availableSpace = layout.width
        }
        return rows
    }
}

private struct SizeStorePreferenceKey: PreferenceKey {
    static var defaultValue: [CGSize] = []

    static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
        value += nextValue()
    }
}

private struct SizeStoreModifier: ViewModifier {
    func body(content: Content) -> some View {
        content.background(
            GeometryReader { geometry in
                Color.clear
                    .preference(
                        key: SizeStorePreferenceKey.self,
                        value: [geometry.size]
                    )
            }
        )
    }
}

private struct RowStorePreferenceKey<T>: PreferenceKey {
    typealias Row = [T]
    typealias Value = [Row]
    static var defaultValue: Value {
        [Row]()
    }

    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue()
    }
}

private extension View {
    func calculateSize() -> some View {
        modifier(SizeStoreModifier())
    }
}
于 2021-09-16T15:06:34.523 回答